Files
dashcaddy/dashcaddy-installer/src/main/caddyfile-generator.js
T
Hermes 84edb035e3
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
[grade=B] feat(auth): onboard missing credentials into encrypted vault
2026-08-22 05:41:38 -07:00

419 lines
12 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { DEFAULT_PORTS } = require('../shared/constants');
/**
* CaddyfileGenerator - Creates Caddyfile and docker-compose.yml for DashCaddy
*
* Generates production-grade configs that match the patterns used by the
* running DashCaddy deployment (CORS snippets, admin origins, PKI, etc.)
*
* DISK SAFETY: All generated configs include sensible defaults for storage
* limits — health retention, stats caps, and memory limits — so a fresh
* install will never silently fill a user's disk.
*/
class CaddyfileGenerator {
/**
* Normalize a Windows path to forward slashes for Caddyfile/Docker Compose
*/
_p(s) {
return s.replace(/\\/g, '/');
}
/**
* Generate the CORS snippet block used by the API
*/
_corsSnippets() {
return `(cors-preflight) {
@preflight method OPTIONS
header @preflight {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers "*"
Access-Control-Max-Age "600"
}
respond @preflight 204
}
(cors-allow) {
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers "*"
Access-Control-Max-Age "600"
}
}
`;
}
/**
* Generate the dashcaddy_auth snippet for SSO
*/
_authSnippet(apiPort) {
return `# DashCaddy SSO auth snippet
(dashcaddy_auth) {
@needsAuth not path /dashcaddy-sso
forward_auth @needsAuth localhost:${apiPort} {
uri /api/v1/auth/gate/{args[0]}
copy_headers Authorization X-Api-Key X-App-Cookie X-Emby-Token X-Plex-Token
}
handle /dashcaddy-sso {
rewrite * /api/v1/auth/sso-exchange
reverse_proxy localhost:${apiPort}
}
}
`;
}
/**
* Generate the dashboard site block content (shared across all modes)
*/
_dashboardBlock(dashboardPath, tier, apiPort) {
let block = '';
block += ` root * ${this._p(dashboardPath)}\n`;
block += ` encode gzip\n\n`;
// API proxy for intermediate/advanced tiers
if (tier !== 'basic') {
block += ` # API proxy to Docker container\n`;
block += ` handle /api/* {\n`;
block += ` reverse_proxy localhost:${apiPort}\n`;
block += ` }\n\n`;
}
// SPA fallback with file_server
block += ` # Static site + SPA fallback\n`;
block += ` handle {\n`;
block += ` @notFile not file {path}\n`;
block += ` rewrite @notFile /index.html\n`;
block += ` file_server\n`;
block += ` }\n`;
return block;
}
/**
* Generate Caddyfile for local mode (IP:Port, no TLS)
*/
generateCaddyfile(options) {
const {
dashboardPath,
port = 8080,
tier = 'basic',
apiPort = DEFAULT_PORTS.API
} = options;
const adminPort = DEFAULT_PORTS.CADDY_ADMIN;
let caddyfile = `# DashCaddy Caddyfile - Local Mode
# Generated by DashCaddy Installer
{
admin localhost:${adminPort} {
origins localhost localhost:${adminPort} host.docker.internal host.docker.internal:${adminPort}
}
auto_https off
}
`;
if (tier !== 'basic') {
caddyfile += this._corsSnippets() + '\n';
}
caddyfile += `:${port} {\n`;
caddyfile += this._dashboardBlock(dashboardPath, tier, apiPort);
caddyfile += `}\n`;
return caddyfile;
}
/**
* Generate Caddyfile for public domain mode (Let's Encrypt TLS)
*/
generateCaddyfileWithDomain(options) {
const {
dashboardPath,
domain,
email = '',
tier = 'basic',
apiPort = DEFAULT_PORTS.API
} = options;
const adminPort = DEFAULT_PORTS.CADDY_ADMIN;
let caddyfile = `# DashCaddy Caddyfile - Public Domain Mode
# Generated by DashCaddy Installer
{
admin localhost:${adminPort} {
origins localhost localhost:${adminPort} host.docker.internal host.docker.internal:${adminPort}
}
`;
if (email) {
caddyfile += ` email ${email}\n`;
}
caddyfile += `}\n\n`;
if (tier !== 'basic') {
caddyfile += this._corsSnippets() + '\n';
caddyfile += this._authSnippet(apiPort) + '\n';
}
caddyfile += `${domain} {\n`;
caddyfile += this._dashboardBlock(dashboardPath, tier, apiPort);
caddyfile += `}\n`;
return caddyfile;
}
/**
* Generate Caddyfile for custom TLD mode (internal CA)
*/
generateCaddyfileCustomTLD(options) {
const {
dashboardPath,
installPath,
tld = '.home',
caName = 'DashCaddy Local CA',
tier = 'basic',
apiPort = DEFAULT_PORTS.API
} = options;
const adminPort = DEFAULT_PORTS.CADDY_ADMIN;
const dashboardDomain = `dashcaddy${tld}`;
const certsPath = installPath ? this._p(path.join(installPath, 'certs')) : './certs';
let caddyfile = `# DashCaddy Caddyfile - Custom TLD Mode (${tld})
# Generated by DashCaddy Installer
{
storage file_system {
root ${certsPath}
}
admin localhost:${adminPort} {
origins localhost localhost:${adminPort} host.docker.internal host.docker.internal:${adminPort}
}
pki {
ca local {
name "${caName}"
root_cn "${caName} Root CA"
intermediate_cn "${caName} Intermediate CA"
}
}
}
`;
if (tier !== 'basic') {
caddyfile += this._corsSnippets() + '\n';
caddyfile += this._authSnippet(apiPort) + '\n';
}
caddyfile += `# Dashboard - ${dashboardDomain}\n`;
caddyfile += `${dashboardDomain} {\n`;
caddyfile += ` tls internal\n\n`;
caddyfile += this._dashboardBlock(dashboardPath, tier, apiPort);
caddyfile += `}\n`;
return caddyfile;
}
/**
* Write Caddyfile to disk
*/
async writeCaddyfile(content, outputPath) {
try {
await fs.promises.writeFile(outputPath, content, 'utf8');
return { success: true, path: outputPath };
} catch (error) {
return { success: false, error: error.message };
}
}
/**
* Create complete Caddyfile setup
* @param {string} installPath - Installation directory
* @param {Object} options - Configuration options
*/
async createCaddyfileSetup(installPath, options = {}) {
try {
const dashboardPath = path.join(installPath, 'sites', 'status');
const caddyfilePath = path.join(installPath, 'Caddyfile');
let content;
if (options.domainMode === 'public') {
content = this.generateCaddyfileWithDomain({
dashboardPath,
domain: options.publicDomain,
email: options.email,
tier: options.tier,
apiPort: options.apiPort
});
} else if (options.domainMode === 'custom-tld') {
content = this.generateCaddyfileCustomTLD({
dashboardPath,
installPath,
tld: options.tld,
caName: options.caName || 'DashCaddy Local CA',
tier: options.tier,
apiPort: options.apiPort
});
} else {
content = this.generateCaddyfile({
dashboardPath,
...options
});
}
const result = await this.writeCaddyfile(content, caddyfilePath);
if (!result.success) {
throw new Error(result.error);
}
return {
success: true,
path: caddyfilePath,
content
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* Generate docker-compose.yml for running the API server.
*
* DISK SAFETY: Includes env vars for health retention, stats caps, and
* memory limits derived from the disk budget the user selected during
* install. These prevent the disk-explosion bugs seen in early versions.
*
* @param {string} installPath - Installation directory
* @param {Object} options - Configuration options
* @param {number} options.apiPort - API server port
* @param {string} options.lanIP - Host LAN IP address
* @param {string} options.tailscaleIP - Host Tailscale IP address
* @param {string} options.domainMode - Domain mode (local, public, custom-tld)
* @param {Object} [options.disk] - Disk budget settings
* @param {number} [options.disk.healthRetentionDays=14] - Health history retention
* @param {number} [options.disk.healthMaxEntries=500] - Max health entries per service
* @param {number} [options.disk.healthCheckInterval=30000] - Health check interval (ms)
* @param {number} [options.disk.statsMaxEntries=2000] - Max container stats entries
* @param {number} [options.disk.auditMaxEntries=1000] - Max audit log entries
* @param {number} [options.disk.backupLimitGB=10] - Backup storage limit
* @param {string} [options.dockerDataPath] - Docker data root override
* @param {number} [options.memoryLimitMB=1024] - Container memory limit
*/
generateDockerCompose(installPath, options = {}) {
const apiPort = options.apiPort || DEFAULT_PORTS.API;
const adminPort = DEFAULT_PORTS.CADDY_ADMIN;
const p = this._p.bind(this);
// Disk budget settings with safe defaults
const disk = options.disk || {};
const healthRetentionDays = disk.healthRetentionDays || 14;
const healthMaxEntries = disk.healthMaxEntries || 500;
const healthCheckInterval = disk.healthCheckInterval || 30000;
const statsMaxEntries = disk.statsMaxEntries || 2000;
const auditMaxEntries = disk.auditMaxEntries || 1000;
const backupLimitGB = disk.backupLimitGB || 10;
const memoryLimitMB = options.memoryLimitMB || 1024;
// Core volume mounts
let volumes = ` - ${p(installPath)}/Caddyfile:/caddyfile:rw
- ${p(installPath)}/services.json:/app/services.json:rw
- ${p(installPath)}/dns-credentials.json:/app/dns-credentials.json:rw
- ${p(installPath)}/config.json:/app/config.json:rw
- ${p(installPath)}/credentials.json:/app/credentials.json:rw
- ${p(installPath)}/.encryption-key:/app/.encryption-key:rw
- ${p(installPath)}/notifications.json:/app/notifications.json:rw
- ${p(installPath)}/sites/status/assets:/app/assets:rw
- /var/run/docker.sock:/var/run/docker.sock`;
// Add CA cert mount for custom-tld mode
if (options.domainMode === 'custom-tld') {
volumes += `\n - ${p(installPath)}/certs/pki/authorities/local:/app/pki:ro`;
}
// Environment variables — disk safety baked in
let envVars = ` - CADDYFILE_PATH=/caddyfile
- CADDY_ADMIN_URL=http://host.docker.internal:${adminPort}
- ASSETS_PATH=/app/assets
- CREDENTIALS_FILE=/app/credentials.json
- NODE_ENV=production
# --- Disk Safety ---
- HEALTH_HISTORY_RETENTION=${healthRetentionDays}
- HEALTH_MAX_ENTRIES=${healthMaxEntries}
- HEALTH_CHECK_INTERVAL=${healthCheckInterval}
- CONTAINER_STATS_MAX_ENTRIES=${statsMaxEntries}
- AUDIT_MAX_ENTRIES=${auditMaxEntries}
- BACKUP_MAX_STORAGE_BYTES=${backupLimitGB * 1024 * 1024 * 1024}`;
if (options.domainMode === 'custom-tld') {
envVars += `\n - CA_CERT_PATH=/app/pki/root.crt`;
}
if (options.lanIP) {
envVars += `\n - HOST_LAN_IP=${options.lanIP}`;
}
if (options.tailscaleIP) {
envVars += `\n - HOST_TAILSCALE_IP=${options.tailscaleIP}`;
}
const dockerCompose = `services:
dashcaddy-api:
build: .
container_name: dashcaddy-api
ports:
- "${apiPort}:${apiPort}"
volumes:
${volumes}
environment:
${envVars}
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
# Memory limit prevents OOM during startup when all managers init
mem_limit: ${memoryLimitMB}m
memswap_limit: ${(memoryLimitMB * 2)}m
`;
return dockerCompose;
}
/**
* Write docker-compose.yml to disk
* @param {string} installPath - Installation directory
* @param {Object} options - Configuration options
*/
async createDockerCompose(installPath, options = {}) {
try {
const content = this.generateDockerCompose(installPath, options);
const apiDir = path.join(installPath, 'sites', 'dashcaddy-api');
await fs.promises.mkdir(apiDir, { recursive: true });
const outputPath = path.join(apiDir, 'docker-compose.yml');
await fs.promises.writeFile(outputPath, content, 'utf8');
return {
success: true,
path: outputPath,
content
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
module.exports = CaddyfileGenerator;