// Cross-platform Node.js API server for Caddy management // Uses Caddy Admin API and Technitium DNS API directly // Run with: node caddy-api.js const express = require('express'); const path = require('path'); const cors = require('cors'); const fs = require('fs'); const app = express(); const PORT = 3001; // Middleware app.use(cors()); app.use(express.json()); app.use(express.static('public')); // Configuration const CADDY_ADMIN_API = process.env.CADDY_ADMIN_API || 'http://localhost:2019'; const DNS_SERVER_API = process.env.DNS_SERVER_API || 'http://192.168.254.204:5380'; const DNS_API_TOKEN = process.env.TECHNITIUM_API_TOKEN || ''; // Helper function to make HTTP requests async function makeRequest(url, options = {}) { const https = url.startsWith('https:') ? require('https') : require('http'); const urlObj = new URL(url); return new Promise((resolve, reject) => { const reqOptions = { hostname: urlObj.hostname, port: urlObj.port, path: urlObj.pathname + urlObj.search, method: options.method || 'GET', headers: options.headers || {}, ...options }; const req = https.request(reqOptions, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { try { const parsed = JSON.parse(data); resolve({ status: res.statusCode, data: parsed }); } catch (e) { resolve({ status: res.statusCode, data: data }); } }); }); req.on('error', reject); if (options.body) { req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body)); } req.end(); }); } // Get current Caddy configuration app.get('/api/caddy/config', async (req, res) => { try { const response = await makeRequest(`${CADDY_ADMIN_API}/config/`); if (response.status === 200) { res.json({ status: 'success', config: response.data }); } else { res.status(response.status).json({ status: 'error', message: 'Failed to get Caddy configuration', details: response.data }); } } catch (error) { console.error('Error getting Caddy config:', error); res.status(500).json({ status: 'error', message: error.message }); } }); // Get list of services (from apps.json + custom apps) app.get('/api/services', async (req, res) => { try { const servicesPath = path.join(__dirname, '../apps.json'); if (fs.existsSync(servicesPath)) { const servicesData = fs.readFileSync(servicesPath, 'utf8'); const services = JSON.parse(servicesData); res.json({ status: 'success', services }); } else { res.json({ status: 'success', services: [] }); } } catch (error) { console.error('Error reading services:', error); res.status(500).json({ status: 'error', message: error.message }); } }); // Add DNS record via Technitium API async function addDnsRecord(domain, ipAddress, ttl = 3600) { if (!DNS_API_TOKEN) { throw new Error('DNS API token not configured. Set TECHNITIUM_API_TOKEN environment variable.'); } const url = `${DNS_SERVER_API}/api/zones/records/add?token=${DNS_API_TOKEN}&domain=${domain}&type=A&ipAddress=${ipAddress}&ttl=${ttl}`; console.log('Adding DNS record:', { domain, ipAddress, ttl }); const response = await makeRequest(url); if (response.data.status === 'ok') { return { success: true, message: 'DNS record added successfully' }; } else { throw new Error(`DNS API error: ${response.data.message || 'Unknown error'}`); } } // Delete DNS record via Technitium API async function deleteDnsRecord(domain, ipAddress) { if (!DNS_API_TOKEN) { console.warn('DNS API token not configured. Skipping DNS deletion.'); return { success: true, message: 'DNS deletion skipped (no token)' }; } const url = `${DNS_SERVER_API}/api/zones/records/delete?token=${DNS_API_TOKEN}&domain=${domain}&type=A&ipAddress=${ipAddress}`; console.log('Deleting DNS record:', { domain, ipAddress }); const response = await makeRequest(url); if (response.data.status === 'ok') { return { success: true, message: 'DNS record deleted successfully' }; } else { throw new Error(`DNS API error: ${response.data.message || 'Unknown error'}`); } } // Add route to Caddy via Admin API async function addCaddyRoute(domain, upstreamUrl, useTls = true) { // Build Caddy route configuration const routeConfig = { "@id": domain, "match": [{ "host": [domain] }], "handle": [{ "handler": "reverse_proxy", "upstreams": [{ "dial": upstreamUrl.replace(/^https?:\/\//, '') }] }], "terminal": true }; // If using internal TLS, we need to add TLS configuration if (useTls) { // Caddy handles TLS automatically for matched domains // Internal CA is configured in the global Caddyfile } console.log('Adding Caddy route:', JSON.stringify(routeConfig, null, 2)); // Add the route to the HTTP server const response = await makeRequest(`${CADDY_ADMIN_API}/config/apps/http/servers/srv0/routes/0`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(routeConfig) }); if (response.status === 200 || response.status === 201) { return { success: true, message: 'Caddy route added successfully' }; } else { throw new Error(`Caddy API error: ${JSON.stringify(response.data)}`); } } // Deploy app endpoint - handles DNS and Caddy configuration via APIs app.post('/api/apps/deploy', async (req, res) => { try { const { appId, config } = req.body; const { subdomain, ip, createDns, port, sslType, dnsType } = config; console.log('Deploying app:', { appId, config }); // Validate required fields if (!appId || !subdomain || !ip) { return res.status(400).json({ success: false, message: 'Missing required fields: appId, subdomain, ip' }); } // Build the full domain const domain = subdomain.includes('.') ? subdomain : `${subdomain}.sami`; const finalPort = port || '80'; const upstreamUrl = `${ip}:${finalPort}`; // Step 1: Add DNS record if requested (private DNS) if (createDns && dnsType === 'private') { try { await addDnsRecord(domain, ip); } catch (dnsError) { console.error('DNS creation failed:', dnsError); return res.status(500).json({ success: false, message: `DNS creation failed: ${dnsError.message}`, step: 'dns' }); } } // Step 2: Add route to Caddy via Admin API try { const useTls = sslType === 'internal'; await addCaddyRoute(domain, upstreamUrl, useTls); } catch (caddyError) { console.error('Caddy route addition failed:', caddyError); // Rollback DNS if it was created if (createDns && dnsType === 'private') { try { await deleteDnsRecord(domain, ip); } catch (rollbackError) { console.error('DNS rollback failed:', rollbackError); } } return res.status(500).json({ success: false, message: `Caddy configuration failed: ${caddyError.message}`, step: 'caddy' }); } // Step 3: Return success response res.json({ success: true, message: `App ${appId} deployed successfully`, url: `https://${domain}`, domain: domain, ip: ip, port: finalPort, containerId: null, dnsCreated: createDns && dnsType === 'private', caddyConfigured: true }); } catch (error) { console.error('Deployment error:', error); res.status(500).json({ success: false, message: error.message }); } }); // Delete app endpoint - removes DNS and Caddy configuration app.post('/api/apps/delete', async (req, res) => { try { const { domain, ip } = req.body; if (!domain) { return res.status(400).json({ success: false, message: 'Domain is required' }); } console.log('Deleting app:', { domain, ip }); // Step 1: Remove from Caddy try { const response = await makeRequest(`${CADDY_ADMIN_API}/id/${domain}`, { method: 'DELETE' }); if (response.status !== 200) { console.warn('Caddy route deletion warning:', response.data); } } catch (caddyError) { console.error('Caddy route deletion failed:', caddyError); // Continue anyway to try DNS deletion } // Step 2: Remove DNS record if IP provided if (ip) { try { await deleteDnsRecord(domain, ip); } catch (dnsError) { console.error('DNS deletion failed:', dnsError); return res.status(500).json({ success: false, message: `DNS deletion failed: ${dnsError.message}`, caddyDeleted: true, dnsDeleted: false }); } } res.json({ success: true, message: 'App deleted successfully', domain: domain, caddyDeleted: true, dnsDeleted: !!ip }); } catch (error) { console.error('Deletion error:', error); res.status(500).json({ success: false, message: error.message }); } }); // Test endpoint app.get('/api/caddy/test', (req, res) => { res.json({ status: 'success', message: 'Caddy API is running', timestamp: new Date().toISOString(), platform: process.platform, caddyAdminApi: CADDY_ADMIN_API, dnsServerApi: DNS_SERVER_API, dnsTokenConfigured: !!DNS_API_TOKEN }); }); // Health check app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString() }); }); // Start server app.listen(PORT, () => { console.log(`\n====================================`); console.log(`Caddy API server running on http://localhost:${PORT}`); console.log(`====================================`); console.log(`Caddy Admin API: ${CADDY_ADMIN_API}`); console.log(`DNS Server API: ${DNS_SERVER_API}`); console.log(`DNS Token: ${DNS_API_TOKEN ? '✓ Configured' : '✗ Not configured'}`); console.log(`\nEndpoints:`); console.log(` POST /api/apps/deploy - Deploy an app (DNS + Caddy)`); console.log(` POST /api/apps/delete - Delete an app (DNS + Caddy)`); console.log(` GET /api/services - Get list of services`); console.log(` GET /api/caddy/config - Get current Caddy configuration`); console.log(` GET /api/caddy/test - Test API connectivity`); console.log(` GET /health - Health check`); console.log(`====================================\n`); }); module.exports = app;