Committed by Hermes autonomous QA sprint 2026-08-13. These files were modified during the Aug 12 sprint but never committed.
228 lines
6.3 KiB
JavaScript
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;
|
|
};
|