419 lines
12 KiB
JavaScript
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;
|