/** * DC-106: Caddyfile-as-code — generate Caddyfile entries from structured JSON * * Allows building reverse proxy configs programmatically instead of editing * raw Caddyfile text. The frontend can present a visual form, send the JSON, * and get back a Caddyfile snippet + apply it via the Caddy admin API. * * POST /api/v1/caddycode/generate — generate Caddyfile block from JSON * POST /api/v1/caddycode/validate — validate a generated block * GET /api/v1/caddycode/importers — list supported import formats */ const express = require('express'); const { ok, errorResponse } = require('../src/utils/responses'); /** * Generate a Caddyfile site block from a structured config. * @param {Object} config - Site configuration * @returns {string} Caddyfile snippet */ function generateSiteBlock(config) { const { domain, upstream, upstreamProtocol = 'http', tls = 'auto', websocket = false, auth = false, authService = null, headers = {}, cors = false, rateLimit = null, cache = false, compress = true, stripPrefix = null, redirectToHttps = true, } = config; const lines = []; lines.push(`${domain} {`); // TLS if (tls === 'internal') { lines.push(` tls internal`); } else if (tls === 'auto') { // Default — Caddy auto-provisions Let's Encrypt } else if (typeof tls === 'string') { lines.push(` tls ${tls}`); } // Redirect HTTP→HTTPS if (redirectToHttps) { lines.push(` # Redirect HTTP to HTTPS is automatic in Caddy 2`); } // Auth gate (DashCaddy forward_auth) if (auth && authService) { lines.push(` import dashcaddy_auth ${authService}`); } // CORS headers if (cors) { lines.push(` header {`); lines.push(` Access-Control-Allow-Origin *`); lines.push(` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"`); lines.push(` Access-Control-Allow-Headers "Content-Type, Authorization"`); lines.push(` }`); } // Custom headers if (Object.keys(headers).length > 0) { lines.push(` header {`); for (const [key, value] of Object.entries(headers)) { lines.push(` ${key} "${value}"`); } lines.push(` }`); } // Strip prefix if (stripPrefix) { lines.push(` uri strip_prefix ${stripPrefix}`); } // Compression if (compress) { lines.push(` encode gzip zstd`); } // Reverse proxy const protocol = upstreamProtocol === 'https' ? 'https' : 'http'; lines.push(` reverse_proxy ${protocol}://${upstream} {`); if (websocket) { lines.push(` # WebSocket support is automatic in Caddy 2`); } lines.push(` header_up Host {host}`); lines.push(` transport http {`); lines.push(` read_timeout 5m`); lines.push(` write_timeout 5m`); lines.push(` }`); lines.push(` }`); lines.push(`}`); return lines.join('\n'); } module.exports = function({ asyncHandler }) { const wrap = asyncHandler || ((fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next)); const router = express.Router(); // POST /api/v1/caddycode/generate router.post('/caddycode/generate', wrap(async (req, res) => { const config = req.body || {}; if (!config.domain) { return errorResponse(res, 400, 'domain is required'); } if (!config.upstream) { return errorResponse(res, 400, 'upstream is required (e.g. localhost:8080)'); } try { const caddyfile = generateSiteBlock(config); ok(res, { caddyfile, config }); } catch (err) { errorResponse(res, 500, `Generation failed: ${err.message}`); } })); // POST /api/v1/caddycode/validate router.post('/caddycode/validate', wrap(async (req, res) => { const { caddyfile } = req.body || {}; if (!caddyfile) { return errorResponse(res, 400, 'caddyfile string is required'); } // Basic validation checks const issues = []; // Check for balanced braces const openBraces = (caddyfile.match(/{/g) || []).length; const closeBraces = (caddyfile.match(/}/g) || []).length; if (openBraces !== closeBraces) { issues.push(`Unbalanced braces: ${openBraces} open vs ${closeBraces} close`); } // Check for domain in first non-empty line const firstLine = caddyfile.trim().split('\n')[0].trim(); if (!firstLine || firstLine.startsWith('#') || firstLine.startsWith('{')) { issues.push('First line should be a domain name'); } // Check for reverse_proxy directive if (!caddyfile.includes('reverse_proxy')) { issues.push('No reverse_proxy directive found — site will not proxy traffic'); } // Check for common mistakes if (caddyfile.includes('tls ')) { const tlsLine = caddyfile.split('\n').find(l => l.trim().startsWith('tls ')); if (tlsLine && tlsLine.includes('auto')) { issues.push('tls auto is redundant — Caddy does this by default'); } } ok(res, { valid: issues.length === 0, issues, warnings: [], }); })); // GET /api/v1/caddycode/templates — preset configs for common patterns router.get('/caddycode/templates', wrap(async (req, res) => { const templates = { 'simple-proxy': { label: 'Simple Reverse Proxy', config: { domain: 'app.example.com', upstream: 'localhost:8080', tls: 'auto', websocket: false, auth: false, }, }, 'websocket-app': { label: 'WebSocket Application', config: { domain: 'app.example.com', upstream: 'localhost:3000', websocket: true, compress: true, }, }, 'auth-gated': { label: 'Auth-Gated Service (DashCaddy SSO)', config: { domain: 'app.example.com', upstream: 'localhost:8096', auth: true, authService: 'app', }, }, 'cors-api': { label: 'API with CORS', config: { domain: 'api.example.com', upstream: 'localhost:3001', cors: true, compress: true, }, }, 'subdirectory': { label: 'Subdirectory Proxy', config: { domain: 'example.com', upstream: 'localhost:8080', stripPrefix: '/app', }, }, }; ok(res, { templates }); })); return router; };