Files
dashcaddy/dashcaddy-api/routes/caddycode.js
T
Hermes 7f831510bd
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
[grade=B] DC-106: Caddyfile-as-code — visual reverse proxy builder API
3 endpoints:
- POST /api/v1/caddycode/generate — generate Caddyfile block from JSON config
  (supports: TLS, auth gate, CORS, headers, WebSocket, compression, strip prefix)
- POST /api/v1/caddycode/validate — validate Caddyfile syntax (brace balance,
  domain check, reverse_proxy presence)
- GET  /api/v1/caddycode/templates — 5 preset configs (simple, WebSocket,
  auth-gated, CORS API, subdirectory)

Frontend can present a visual form, send JSON, get back Caddyfile snippet.
1633 tests pass.
2026-08-12 12:54:17 -07:00

228 lines
6.3 KiB
JavaScript

/**
* 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;
};