Files
onebox/ts/onebox.classes.nginx.ts
Juergen Kunz 246a6073e0 Initial commit: Onebox v1.0.0
- Complete Deno-based architecture following nupst/spark patterns
- SQLite database with full schema
- Docker container management
- Service orchestration (Docker + Nginx + DNS + SSL)
- Registry authentication
- Nginx reverse proxy configuration
- Cloudflare DNS integration
- Let's Encrypt SSL automation
- Background daemon with metrics collection
- HTTP API server
- Comprehensive CLI
- Cross-platform compilation setup
- NPM distribution wrapper
- Shell installer script

Core features:
- Deploy containers with single command
- Automatic domain configuration
- Automatic SSL certificates
- Multi-registry support
- Metrics and logging
- Systemd integration

Ready for Angular UI implementation and testing.
2025-10-28 13:05:42 +00:00

346 lines
9.4 KiB
TypeScript

/**
* Nginx Manager for Onebox
*
* Manages Nginx reverse proxy configurations for services
*/
import * as plugins from './onebox.plugins.ts';
import { logger } from './onebox.logging.ts';
import { OneboxDatabase } from './onebox.classes.database.ts';
export class OneboxNginxManager {
private oneboxRef: any;
private database: OneboxDatabase;
private configDir = '/etc/nginx/sites-available';
private enabledDir = '/etc/nginx/sites-enabled';
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
// Allow custom nginx config directory
const customDir = this.database.getSetting('nginxConfigDir');
if (customDir) {
this.configDir = customDir;
}
}
/**
* Initialize nginx manager
*/
async init(): Promise<void> {
try {
// Ensure directories exist
await Deno.mkdir(this.configDir, { recursive: true });
await Deno.mkdir(this.enabledDir, { recursive: true });
logger.info('Nginx manager initialized');
} catch (error) {
logger.error(`Failed to initialize Nginx manager: ${error.message}`);
throw error;
}
}
/**
* Create nginx config for a service
*/
async createConfig(serviceId: number, domain: string, port: number): Promise<void> {
try {
logger.info(`Creating Nginx config for ${domain}`);
const service = this.database.getServiceByID(serviceId);
if (!service) {
throw new Error(`Service not found: ${serviceId}`);
}
// Get container IP (or use container name for DNS resolution within Docker network)
const containerName = `onebox-${service.name}`;
// Generate config
const config = this.generateConfig(domain, containerName, port);
// Write config file
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
await Deno.writeTextFile(configPath, config);
// Create symlink in sites-enabled
const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`;
try {
await Deno.remove(enabledPath);
} catch {
// Ignore if doesn't exist
}
await Deno.symlink(configPath, enabledPath);
logger.success(`Nginx config created: ${domain}`);
} catch (error) {
logger.error(`Failed to create Nginx config: ${error.message}`);
throw error;
}
}
/**
* Generate nginx configuration
*/
private generateConfig(domain: string, upstream: string, port: number): string {
return `# Onebox-managed configuration for ${domain}
# Generated at ${new Date().toISOString()}
upstream onebox_${domain.replace(/\./g, '_')} {
server ${upstream}:${port};
}
# HTTP server (redirect to HTTPS or serve directly)
server {
listen 80;
listen [::]:80;
server_name ${domain};
# ACME challenge for Let's Encrypt
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect to HTTPS (will be enabled after SSL is configured)
# location / {
# return 301 https://$server_name$request_uri;
# }
# Proxy to container (remove after SSL is configured)
location / {
proxy_pass http://onebox_${domain.replace(/\./g, '_')};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
# HTTPS server (uncomment after SSL is configured)
# server {
# listen 443 ssl http2;
# listen [::]:443 ssl http2;
# server_name ${domain};
#
# ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
#
# # SSL configuration
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
#
# location / {
# proxy_pass http://onebox_${domain.replace(/\./g, '_')};
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
#
# # WebSocket support
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
#
# # Timeouts
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout 60s;
# }
# }
`;
}
/**
* Update nginx config to enable SSL
*/
async enableSSL(domain: string): Promise<void> {
try {
logger.info(`Enabling SSL for ${domain}`);
// Find service by domain
const services = this.database.getAllServices();
const service = services.find((s) => s.domain === domain);
if (!service) {
throw new Error(`Service not found for domain: ${domain}`);
}
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
let config = await Deno.readTextFile(configPath);
// Enable HTTPS redirect and HTTPS server block
config = config.replace('# location / {\n # return 301', 'location / {\n return 301');
config = config.replace('# }', '}');
config = config.replace(/# server \{[\s\S]*?# \}/m, (match) =>
match.replace(/# /g, '')
);
// Comment out HTTP proxy location
config = config.replace(
/# Proxy to container \(remove after SSL is configured\)[\s\S]*?location \/ \{[\s\S]*?\n \}/,
(match) => `# ${match.replace(/\n/g, '\n # ')}`
);
await Deno.writeTextFile(configPath, config);
logger.success(`SSL enabled for ${domain}`);
} catch (error) {
logger.error(`Failed to enable SSL for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Remove nginx config for a service
*/
async removeConfig(serviceId: number): Promise<void> {
try {
const service = this.database.getServiceByID(serviceId);
if (!service) {
throw new Error(`Service not found: ${serviceId}`);
}
logger.info(`Removing Nginx config for ${service.name}`);
// Remove symlink
const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`;
try {
await Deno.remove(enabledPath);
} catch {
// Ignore if doesn't exist
}
// Remove config file
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
try {
await Deno.remove(configPath);
} catch {
// Ignore if doesn't exist
}
logger.success(`Nginx config removed for ${service.name}`);
} catch (error) {
logger.error(`Failed to remove Nginx config: ${error.message}`);
throw error;
}
}
/**
* Test nginx configuration
*/
async test(): Promise<boolean> {
try {
const command = new Deno.Command('nginx', {
args: ['-t'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
logger.error(`Nginx config test failed: ${errorMsg}`);
return false;
}
logger.success('Nginx configuration is valid');
return true;
} catch (error) {
logger.error(`Failed to test Nginx config: ${error.message}`);
return false;
}
}
/**
* Reload nginx
*/
async reload(): Promise<void> {
try {
// Test config first
const isValid = await this.test();
if (!isValid) {
throw new Error('Nginx configuration is invalid');
}
logger.info('Reloading Nginx...');
const command = new Deno.Command('systemctl', {
args: ['reload', 'nginx'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stderr } = await command.output();
if (code !== 0) {
const errorMsg = new TextDecoder().decode(stderr);
throw new Error(`Nginx reload failed: ${errorMsg}`);
}
logger.success('Nginx reloaded successfully');
} catch (error) {
logger.error(`Failed to reload Nginx: ${error.message}`);
throw error;
}
}
/**
* Get nginx status
*/
async getStatus(): Promise<string> {
try {
const command = new Deno.Command('systemctl', {
args: ['status', 'nginx'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
const output = new TextDecoder().decode(stdout);
if (code === 0 || output.includes('active (running)')) {
return 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
return 'stopped';
} else if (output.includes('failed')) {
return 'failed';
} else {
return 'unknown';
}
} catch (error) {
logger.error(`Failed to get Nginx status: ${error.message}`);
return 'unknown';
}
}
/**
* Check if nginx is installed
*/
async isInstalled(): Promise<boolean> {
try {
const command = new Deno.Command('which', {
args: ['nginx'],
stdout: 'piped',
stderr: 'piped',
});
const { code } = await command.output();
return code === 0;
} catch {
return false;
}
}
}