update
This commit is contained in:
317
ts/classes/ssl.ts
Normal file
317
ts/classes/ssl.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* SSL Manager for Onebox
|
||||
*
|
||||
* Manages SSL certificates via Let's Encrypt (using smartacme)
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import { OneboxDatabase } from './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<void> {
|
||||
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 <email>');
|
||||
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<void> {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain certificate using certbot
|
||||
*/
|
||||
private async obtainCertificateWithCertbot(domain: string): Promise<void> {
|
||||
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<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}`);
|
||||
}
|
||||
|
||||
// Update database
|
||||
const expiryDate = Date.now() + 90 * 24 * 60 * 60 * 1000;
|
||||
this.database.updateSSLCertificate(domain, {
|
||||
expiryDate,
|
||||
});
|
||||
|
||||
logger.success(`Certificate renewed for ${domain}`);
|
||||
|
||||
// Reload certificates in reverse proxy
|
||||
await this.oneboxRef.reverseProxy.reloadCertificates();
|
||||
} 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<void> {
|
||||
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<void> {
|
||||
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 certificates in reverse proxy
|
||||
await this.oneboxRef.reverseProxy.reloadCertificates();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to renew all certificates: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if certbot is installed
|
||||
*/
|
||||
async isCertbotInstalled(): Promise<boolean> {
|
||||
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<Date | null> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user