// Konfigurations-Generator für Webserver-Sites (nginx, Apache, Caddy): Optionsschema, Vorlagen und Erzeugung. // Reine Funktionen ohne DOM – im Renderer global (WEBCONF), in Node per require (Tests). (function (root) { 'use strict'; // Optionsschema. type: bool | text | number | select | list | textarea // only: Server, die die Option unterstützen; kw: Suchbegriffe (z. B. Direktiven); show: Sichtbarkeit abhängig von anderen Werten const GROUPS = [ ['basics', 'Basics'], ['proxy', 'Reverse proxy'], ['tls', 'HTTPS / TLS'], ['security', 'Security'], ['access', 'Access control'], ['performance', 'Performance & caching'], ['php', 'PHP'], ['redirect', 'Redirect'], ['logging', 'Logging'], ['custom', 'Custom directives'], ]; const isType = (...t) => (o) => t.includes(o.type); const OPTIONS = [ { id: 'type', group: 'basics', label: 'Site type', type: 'select', def: 'proxy', options: [['proxy', 'Reverse proxy'], ['static', 'Static website'], ['spa', 'Single-page app (index.html fallback)'], ['php', 'PHP application'], ['redirect', 'Redirect to another URL']], kw: 'mode kind' }, { id: 'domains', group: 'basics', label: 'Domains', desc: 'Separated by spaces. The first one is the main name.', type: 'text', def: '', placeholder: 'example.com www.example.com', kw: 'server_name ServerName ServerAlias host' }, { id: 'name', group: 'basics', label: 'Name of the configuration file', type: 'text', def: '', placeholder: 'optional – default: first domain', only: ['nginx', 'apache'] }, { id: 'httpPort', group: 'basics', label: 'HTTP port', type: 'number', def: 80, kw: 'listen' }, { id: 'root', group: 'basics', label: 'Document root', type: 'text', def: '', placeholder: '/var/www/example.com', show: isType('static', 'spa', 'php'), kw: 'root DocumentRoot' }, { id: 'createRoot', group: 'basics', label: 'Create the folder with a placeholder page if it does not exist', type: 'bool', def: true, show: isType('static', 'spa', 'php') }, { id: 'index', group: 'basics', label: 'Index files', type: 'text', def: 'index.html index.htm', show: isType('static', 'spa', 'php'), kw: 'index DirectoryIndex' }, { id: 'upstreams', group: 'proxy', label: 'Target servers (upstream)', desc: 'One or more, separated by spaces. Several targets = load balancing.', type: 'text', def: 'http://127.0.0.1:3000', show: isType('proxy'), kw: 'proxy_pass ProxyPass reverse_proxy backend balancer' }, { id: 'proxyPath', group: 'proxy', label: 'Path', desc: 'Only requests below this path are forwarded.', type: 'text', def: '/', show: isType('proxy'), kw: 'location' }, { id: 'lb', group: 'proxy', label: 'Load balancing method', type: 'select', def: 'round_robin', options: [['round_robin', 'Round robin'], ['least_conn', 'Least connections'], ['ip_hash', 'Same client → same server (IP hash)']], show: (o) => o.type === 'proxy' && splitList(o.upstreams).length > 1, kw: 'least_conn ip_hash lbmethod lb_policy' }, { id: 'websockets', group: 'proxy', label: 'WebSockets', desc: 'Forward Upgrade/Connection headers.', type: 'bool', def: true, show: isType('proxy'), kw: 'upgrade ws wss' }, { id: 'preserveHost', group: 'proxy', label: 'Pass the original Host header', type: 'bool', def: true, show: isType('proxy'), kw: 'ProxyPreserveHost header_up Host' }, { id: 'forwardHeaders', group: 'proxy', label: 'X-Forwarded-For / -Proto / X-Real-IP', type: 'bool', def: true, show: isType('proxy'), only: ['nginx', 'apache'], kw: 'X-Forwarded-For real ip' }, { id: 'proxyTimeout', group: 'proxy', label: 'Timeout (seconds)', desc: 'Read timeout for slow backends. 0 = default.', type: 'number', def: 60, show: isType('proxy'), kw: 'proxy_read_timeout ProxyTimeout' }, { id: 'proxyBuffering', group: 'proxy', label: 'Buffer responses', desc: 'Turn off for streaming (Server-Sent Events, long polling).', type: 'bool', def: true, show: isType('proxy'), kw: 'proxy_buffering flush_interval sse' }, { id: 'upstreamInsecure', group: 'proxy', label: 'Do not verify the HTTPS certificate of the target', type: 'bool', def: false, show: (o) => o.type === 'proxy' && /https:/.test(o.upstreams), kw: 'proxy_ssl_verify SSLProxyVerify tls_insecure_skip_verify' }, { id: 'tls', group: 'tls', label: 'Enable HTTPS', type: 'bool', def: false, kw: 'ssl https certificate 443' }, { id: 'httpsPort', group: 'tls', label: 'HTTPS port', type: 'number', def: 443, show: (o) => o.tls, kw: 'listen 443' }, { id: 'certSource', group: 'tls', label: 'Certificate', type: 'select', def: 'letsencrypt', options: [['letsencrypt', "Let's Encrypt (certbot, /etc/letsencrypt/live/)"], ['auto', 'Automatic (Caddy)'], ['custom', 'Own files']], show: (o) => o.tls, kw: 'ssl_certificate SSLCertificateFile' }, { id: 'certFile', group: 'tls', label: 'Certificate file', type: 'text', def: '', placeholder: '/etc/ssl/mrterm/site.crt', show: (o) => o.tls && o.certSource === 'custom', kw: 'fullchain crt pem' }, { id: 'keyFile', group: 'tls', label: 'Key file', type: 'text', def: '', placeholder: '/etc/ssl/mrterm/site.key', show: (o) => o.tls && o.certSource === 'custom', kw: 'privkey key' }, { id: 'redirectHttp', group: 'tls', label: 'Redirect HTTP to HTTPS', type: 'bool', def: true, show: (o) => o.tls, kw: '301 force https' }, { id: 'http2', group: 'tls', label: 'HTTP/2', type: 'bool', def: true, show: (o) => o.tls, only: ['nginx', 'apache'], kw: 'h2 Protocols' }, { id: 'tlsModern', group: 'tls', label: 'Only TLS 1.2 and 1.3', type: 'bool', def: true, show: (o) => o.tls, only: ['nginx', 'apache'], kw: 'ssl_protocols SSLProtocol' }, { id: 'hsts', group: 'tls', label: 'HSTS (seconds, 0 = off)', desc: 'Browsers then only use HTTPS for this domain. 31536000 = one year.', type: 'number', def: 0, show: (o) => o.tls, kw: 'Strict-Transport-Security' }, { id: 'hideVersion', group: 'security', label: 'Hide server version', type: 'bool', def: true, kw: 'server_tokens ServerSignature Server header' }, { id: 'xFrame', group: 'security', label: 'X-Frame-Options (clickjacking protection)', type: 'select', def: 'SAMEORIGIN', options: [['', 'off'], ['SAMEORIGIN', 'SAMEORIGIN'], ['DENY', 'DENY']], kw: 'iframe frame' }, { id: 'noSniff', group: 'security', label: 'X-Content-Type-Options: nosniff', type: 'bool', def: true, kw: 'mime sniffing' }, { id: 'referrer', group: 'security', label: 'Referrer-Policy', type: 'select', def: 'strict-origin-when-cross-origin', options: [['', 'off'], ['strict-origin-when-cross-origin', 'strict-origin-when-cross-origin'], ['no-referrer', 'no-referrer'], ['same-origin', 'same-origin']], kw: 'referer' }, { id: 'csp', group: 'security', label: 'Content-Security-Policy', type: 'text', def: '', placeholder: "default-src 'self'", kw: 'csp script-src' }, { id: 'permissions', group: 'security', label: 'Permissions-Policy', type: 'text', def: '', placeholder: 'camera=(), microphone=(), geolocation=()', kw: 'feature policy' }, { id: 'denyHidden', group: 'security', label: 'Block hidden files (.git, .env, .htaccess)', type: 'bool', def: true, show: isType('static', 'spa', 'php'), kw: 'dotfiles deny' }, { id: 'allowIps', group: 'access', label: 'Allowed IPs / networks', desc: 'Empty = everyone. Others get 403.', type: 'text', def: '', placeholder: '192.168.0.0/24 10.0.0.5', kw: 'allow deny Require ip remote_ip whitelist' }, { id: 'basicAuth', group: 'access', label: 'Password protection (basic auth)', type: 'bool', def: false, only: ['nginx', 'apache'], kw: 'auth_basic AuthType htpasswd login' }, { id: 'authFile', group: 'access', label: 'Password file (htpasswd)', desc: 'Create it with: htpasswd -c /etc/nginx/.htpasswd user', type: 'text', def: '/etc/nginx/.htpasswd', show: (o) => o.basicAuth, only: ['nginx', 'apache'], kw: 'auth_basic_user_file AuthUserFile' }, { id: 'authRealm', group: 'access', label: 'Realm (text in the login dialog)', type: 'text', def: 'Restricted', show: (o) => o.basicAuth, only: ['nginx', 'apache'], kw: 'AuthName' }, { id: 'rateLimit', group: 'access', label: 'Rate limit (requests per second per IP, 0 = off)', type: 'number', def: 0, only: ['nginx'], kw: 'limit_req throttle ddos' }, { id: 'rateBurst', group: 'access', label: 'Burst', type: 'number', def: 20, show: (o) => Number(o.rateLimit) > 0, only: ['nginx'], kw: 'limit_req burst' }, { id: 'gzip', group: 'performance', label: 'Compression (gzip)', type: 'bool', def: true, kw: 'deflate encode zstd brotli' }, { id: 'cacheDays', group: 'performance', label: 'Browser cache for assets (days, 0 = off)', desc: 'CSS, JS, images and fonts.', type: 'number', def: 30, show: isType('static', 'spa', 'php'), kw: 'expires Cache-Control max-age' }, { id: 'maxBody', group: 'performance', label: 'Max. upload size (MB, 0 = default)', type: 'number', def: 0, kw: 'client_max_body_size LimitRequestBody request_body max_size upload' }, { id: 'phpSocket', group: 'php', label: 'PHP-FPM socket', type: 'text', def: '/run/php/php-fpm.sock', show: isType('php'), kw: 'fastcgi_pass php_fastcgi SetHandler fpm' }, { id: 'redirectTo', group: 'redirect', label: 'Target URL', type: 'text', def: 'https://example.com', show: isType('redirect'), kw: 'return redir Redirect' }, { id: 'redirectCode', group: 'redirect', label: 'Status code', type: 'select', def: '301', options: [['301', '301 (permanent)'], ['302', '302 (temporary)'], ['307', '307'], ['308', '308']], show: isType('redirect') }, { id: 'redirectPath', group: 'redirect', label: 'Keep the path', desc: 'example.org/a → target/a', type: 'bool', def: true, show: isType('redirect'), kw: 'request_uri' }, { id: 'accessLog', group: 'logging', label: 'Access log', desc: 'Empty = default of the server, "off" = no access log.', type: 'text', def: '', placeholder: '/var/log/nginx/example.access.log', kw: 'access_log CustomLog log' }, { id: 'errorLog', group: 'logging', label: 'Error log', type: 'text', def: '', placeholder: '/var/log/nginx/example.error.log', only: ['nginx', 'apache'], kw: 'error_log ErrorLog' }, { id: 'extra', group: 'custom', label: 'Additional directives', desc: 'Inserted as-is into the server block.', type: 'textarea', def: '', kw: 'raw custom snippet' }, ]; const PRESETS = [ ['proxy', 'Reverse proxy (Node, Docker, …)', { type: 'proxy', upstreams: 'http://127.0.0.1:3000', websockets: true }], ['lb', 'Load balancer', { type: 'proxy', upstreams: 'http://10.0.0.11:8080 http://10.0.0.12:8080', lb: 'least_conn' }], ['static', 'Static website', { type: 'static', cacheDays: 30 }], ['spa', 'Single-page app (React, Vue, …)', { type: 'spa', cacheDays: 365 }], ['php', 'PHP app (WordPress, Laravel, …)', { type: 'php', index: 'index.php index.html', maxBody: 64 }], ['redirect', 'Redirect / domain move', { type: 'redirect' }], ['secure', 'Hardened HTTPS site', { tls: true, redirectHttp: true, hsts: 31536000, xFrame: 'DENY', csp: "default-src 'self'", permissions: 'camera=(), microphone=(), geolocation=()' }], ]; function defaults() { return Object.fromEntries(OPTIONS.map((o) => [o.id, o.def])); } function splitList(v) { return String(v || '').split(/[\s,]+/).map((x) => x.trim()).filter(Boolean); } const visible = (opt, o, server) => (!opt.only || opt.only.includes(server)) && (!opt.show || opt.show(o)); const num = (v) => Math.max(0, Number(v) || 0); const indent = (text, n) => text.split('\n').map((l) => (l ? ' '.repeat(n) + l : l)).join('\n'); const hostPort = (u) => u.replace(/^https?:\/\//, '').replace(/\/.*$/, ''); function certPaths(o, domain) { if (o.certSource === 'custom') return { cert: o.certFile || '/etc/ssl/certs/site.crt', key: o.keyFile || '/etc/ssl/private/site.key' }; return { cert: `/etc/letsencrypt/live/${domain}/fullchain.pem`, key: `/etc/letsencrypt/live/${domain}/privkey.pem` }; } const headerList = (o) => [ o.xFrame && ['X-Frame-Options', o.xFrame], o.noSniff && ['X-Content-Type-Options', 'nosniff'], o.referrer && ['Referrer-Policy', o.referrer], o.csp && ['Content-Security-Policy', o.csp], o.permissions && ['Permissions-Policy', o.permissions], o.tls && num(o.hsts) && ['Strict-Transport-Security', `max-age=${num(o.hsts)}; includeSubDomains`], ].filter(Boolean); const ASSET_EXT = 'css|js|mjs|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|eot'; const cmpVersion = (a, b) => { const x = String(a).split('.').map(Number), y = String(b).split('.').map(Number); for (let i = 0; i < 3; i++) { if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) - (y[i] || 0); } return 0; }; const zoneName = (o) => `mrterm_${(splitList(o.domains)[0] || 'site').replace(/[^a-z0-9]/gi, '_')}`; // ---------------------------------------------------------------- nginx function nginx(o, env = {}) { const domains = splitList(o.domains); const main = domains[0] || 'example.com'; const ups = splitList(o.upstreams); const upName = `${zoneName(o).replace(/^mrterm_/, '')}_backend`; const out = ['# Generated by MrTerm']; if (o.type === 'proxy' && ups.length > 1) { out.push(`upstream ${upName} {`, ...(o.lb === 'least_conn' ? [' least_conn;'] : o.lb === 'ip_hash' ? [' ip_hash;'] : []), ...ups.map((u) => ` server ${hostPort(u)};`), '}', ''); } if (num(o.rateLimit)) out.push(`limit_req_zone $binary_remote_addr zone=${zoneName(o)}:10m rate=${num(o.rateLimit)}r/s;`, ''); const body = []; if (o.type !== 'redirect' && o.type !== 'proxy') body.push(`root ${o.root || `/var/www/${main}`};`, `index ${o.index || 'index.html'};`); if (o.hideVersion) body.push('server_tokens off;'); if (num(o.maxBody)) body.push(`client_max_body_size ${num(o.maxBody)}m;`); if (o.accessLog) body.push(o.accessLog === 'off' ? 'access_log off;' : `access_log ${o.accessLog};`); if (o.errorLog) body.push(`error_log ${o.errorLog};`); const hdr = headerList(o); if (hdr.length) body.push('', ...hdr.map(([k, v]) => `add_header ${k} "${v.replace(/"/g, '\\"')}" always;`)); if (o.gzip) body.push('', 'gzip on;', 'gzip_vary on;', 'gzip_proxied any;', 'gzip_comp_level 5;', 'gzip_types text/plain text/css text/xml application/json application/javascript application/xml image/svg+xml;'); const ips = splitList(o.allowIps); if (ips.length) body.push('', ...ips.map((ip) => `allow ${ip};`), 'deny all;'); if (o.basicAuth) body.push('', `auth_basic "${o.authRealm || 'Restricted'}";`, `auth_basic_user_file ${o.authFile || '/etc/nginx/.htpasswd'};`); const limit = num(o.rateLimit) ? [`limit_req zone=${zoneName(o)} burst=${num(o.rateBurst) || 20} nodelay;`] : []; if (o.type === 'redirect') { body.push('', `return ${o.redirectCode || 301} ${(o.redirectTo || 'https://example.com').replace(/\/$/, '')}${o.redirectPath ? '$request_uri' : ''};`); } else if (o.type === 'proxy') { const target = ups.length > 1 ? `${/^https:/.test(ups[0]) ? 'https' : 'http'}://${upName}` : (ups[0] || 'http://127.0.0.1:3000'); const loc = [...limit, `proxy_pass ${target};`]; if (o.websockets || o.preserveHost || o.forwardHeaders) loc.push('proxy_http_version 1.1;'); if (o.preserveHost) loc.push('proxy_set_header Host $host;'); if (o.forwardHeaders) loc.push('proxy_set_header X-Real-IP $remote_addr;', 'proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;', 'proxy_set_header X-Forwarded-Proto $scheme;', 'proxy_set_header X-Forwarded-Host $host;'); if (o.websockets) loc.push('proxy_set_header Upgrade $http_upgrade;', `proxy_set_header Connection $${zoneName(o)}_upgrade;`); if (num(o.proxyTimeout) && num(o.proxyTimeout) !== 60) loc.push(`proxy_connect_timeout ${Math.min(num(o.proxyTimeout), 75)}s;`, `proxy_send_timeout ${num(o.proxyTimeout)}s;`, `proxy_read_timeout ${num(o.proxyTimeout)}s;`); if (!o.proxyBuffering) loc.push('proxy_buffering off;', 'proxy_cache off;'); if (o.upstreamInsecure) loc.push('proxy_ssl_verify off;'); if (/^https:/.test(target)) loc.push('proxy_ssl_server_name on;'); body.push('', `location ${o.proxyPath || '/'} {`, indent(loc.join('\n'), 4), '}'); if (o.websockets) out.push('# WebSocket: Connection header matching the Upgrade header', `map $http_upgrade $${zoneName(o)}_upgrade {`, ' default upgrade;', " '' close;", '}', ''); } else { const fallback = o.type === 'spa' ? '/index.html' : o.type === 'php' ? '/index.php?$query_string' : '=404'; body.push('', 'location / {', indent([...limit, `try_files $uri $uri/ ${fallback};`].join('\n'), 4), '}'); if (o.type === 'php') body.push('', 'location ~ \\.php$ {', ' try_files $uri =404;', ' include fastcgi_params;', ' fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;', ` fastcgi_pass unix:${o.phpSocket || '/run/php/php-fpm.sock'};`, '}'); if (num(o.cacheDays)) body.push('', `location ~* \\.(${ASSET_EXT})$ {`, ` expires ${num(o.cacheDays)}d;`, ' access_log off;', ' try_files $uri =404;', '}'); if (o.denyHidden) body.push('', 'location ~ /\\.(?!well-known) {', ' deny all;', '}'); } if (o.extra?.trim()) body.push('', '# Custom', o.extra.trim()); const listen = (port, extra = '') => [`listen ${port}${extra};`, `listen [::]:${port}${extra};`]; if (o.tls) { const c = certPaths(o, main); if (o.redirectHttp) out.push('server {', indent([...listen(num(o.httpPort) || 80), `server_name ${domains.join(' ') || main};`, 'return 301 https://$host$request_uri;'].join('\n'), 4), '}', ''); const tls = [`ssl_certificate ${c.cert};`, `ssl_certificate_key ${c.key};`]; if (o.tlsModern) tls.push('ssl_protocols TLSv1.2 TLSv1.3;', 'ssl_prefer_server_ciphers off;'); tls.push('ssl_session_cache shared:SSL:10m;', 'ssl_session_timeout 1d;'); // `http2 on;` gibt es erst ab nginx 1.25.1, davor gehört http2 an listen const newH2 = !env.nginxVersion || cmpVersion(env.nginxVersion, '1.25.1') >= 0; out.push('server {', indent([...listen(num(o.httpsPort) || 443, o.http2 && !newH2 ? ' ssl http2' : ' ssl'), ...(o.http2 && newH2 ? ['http2 on;'] : []), `server_name ${domains.join(' ') || main};`, '', ...tls, '', ...body].join('\n'), 4), '}'); if (!o.redirectHttp) out.push('', 'server {', indent([...listen(num(o.httpPort) || 80), `server_name ${domains.join(' ') || main};`, '', ...body].join('\n'), 4), '}'); } else { out.push('server {', indent([...listen(num(o.httpPort) || 80), `server_name ${domains.join(' ') || main};`, '', ...body].join('\n'), 4), '}'); } return { text: `${out.join('\n')}\n`, modules: [] }; } // ---------------------------------------------------------------- Apache function apache(o) { const domains = splitList(o.domains); const main = domains[0] || 'example.com'; const ups = splitList(o.upstreams); const mods = new Set(); const balancer = `balancer://${zoneName(o).replace(/^mrterm_/, '')}`; const pre = []; const body = []; if (o.type !== 'proxy' && o.type !== 'redirect') { const root0 = o.root || `/var/www/${main}`; const dir = ['Options -Indexes +FollowSymLinks', 'AllowOverride All']; const ips = splitList(o.allowIps); dir.push(ips.length ? `Require ip ${ips.join(' ')}` : 'Require all granted'); if (o.type === 'spa') dir.push('FallbackResource /index.html'); body.push(`DocumentRoot ${root0}`, `DirectoryIndex ${o.index || 'index.html'}`, ``, indent(dir.join('\n'), 4), ''); } else if (splitList(o.allowIps).length) { body.push('', ` Require ip ${splitList(o.allowIps).join(' ')}`, ''); } if (o.hideVersion) body.push('ServerSignature Off'); if (num(o.maxBody)) body.push(`LimitRequestBody ${num(o.maxBody) * 1024 * 1024}`); if (o.errorLog) body.push(`ErrorLog ${o.errorLog}`); if (o.accessLog && o.accessLog !== 'off') body.push(`CustomLog ${o.accessLog} combined`); const hdr = headerList(o); if (hdr.length) { mods.add('headers'); body.push('', ...hdr.map(([k, v]) => `Header always set ${k} "${v.replace(/"/g, '\\"')}"`)); } if (o.gzip) { mods.add('deflate'); body.push('', 'AddOutputFilterByType DEFLATE text/plain text/html text/css text/xml application/json application/javascript application/xml image/svg+xml'); } if (o.basicAuth) body.push('', '', ' AuthType Basic', ` AuthName "${o.authRealm || 'Restricted'}"`, ` AuthUserFile ${o.authFile || '/etc/apache2/.htpasswd'}`, ' Require valid-user', ''); if (o.type === 'redirect') { mods.add('alias'); body.push('', o.redirectPath ? `Redirect ${o.redirectCode || 301} / ${(o.redirectTo || 'https://example.com').replace(/\/?$/, '/')}` : `RedirectMatch ${o.redirectCode || 301} ^ ${o.redirectTo || 'https://example.com'}`); } else if (o.type === 'proxy') { mods.add('proxy'); mods.add('proxy_http'); const p = (o.proxyPath || '/').replace(/\/?$/, '/'); // Worker-Parameter: bei balancer:// gehören sie an die einzelnen BalancerMember, nicht an ProxyPass const flags = [o.websockets ? 'upgrade=websocket' : '', o.proxyBuffering ? '' : 'flushpackets=on'].filter(Boolean).join(' '); let target; if (ups.length > 1) { mods.add('proxy_balancer'); mods.add('slotmem_shm'); mods.add(o.lb === 'least_conn' ? 'lbmethod_bybusyness' : 'lbmethod_byrequests'); pre.push(``, ...ups.map((u) => ` BalancerMember ${u.replace(/\/$/, '')}${flags ? ` ${flags}` : ''}`), ` ProxySet lbmethod=${o.lb === 'least_conn' ? 'bybusyness' : 'byrequests'}`, ''); target = `${balancer}/`; } else target = (ups[0] || 'http://127.0.0.1:3000').replace(/\/?$/, '/'); if (o.preserveHost) body.push('', 'ProxyPreserveHost On'); if (o.forwardHeaders) { mods.add('headers'); body.push('RequestHeader set X-Forwarded-Proto "expr=%{REQUEST_SCHEME}"'); } if (num(o.proxyTimeout) && num(o.proxyTimeout) !== 60) body.push(`ProxyTimeout ${num(o.proxyTimeout)}`); if (/^https:/.test(target) || ups.some((u) => /^https:/.test(u))) { mods.add('ssl'); body.push('SSLProxyEngine On'); if (o.upstreamInsecure) body.push('SSLProxyVerify none', 'SSLProxyCheckPeerName off', 'SSLProxyCheckPeerExpire off'); } if (pre.length) body.push(...pre, ''); body.push(`ProxyPass ${p} ${target}${flags && ups.length <= 1 ? ` ${flags}` : ''}`, `ProxyPassReverse ${p} ${target}`); } else { if (o.type === 'php') { mods.add('proxy'); mods.add('proxy_fcgi'); body.push('', '', ` SetHandler "proxy:unix:${o.phpSocket || '/run/php/php-fpm.sock'}|fcgi://localhost"`, ''); } if (num(o.cacheDays)) { mods.add('expires'); body.push('', '', ' ExpiresActive On', ...['text/css', 'application/javascript', 'image/png', 'image/jpeg', 'image/gif', 'image/svg+xml', 'image/webp', 'font/woff2'].map((t) => ` ExpiresByType ${t} "access plus ${num(o.cacheDays)} days"`), ''); } if (o.denyHidden) body.push('', '', ' Require all denied', ''); } if (o.extra?.trim()) body.push('', '# Custom', o.extra.trim()); const names = [`ServerName ${main}`, ...(domains.length > 1 ? [`ServerAlias ${domains.slice(1).join(' ')}`] : [])]; const out = ['# Generated by MrTerm']; const vhost = (port, lines) => [``, indent([...names, '', ...lines].join('\n'), 4), '']; if (o.tls) { mods.add('ssl'); const c = certPaths(o, main); const tls = ['SSLEngine on', `SSLCertificateFile ${c.cert}`, `SSLCertificateKeyFile ${c.key}`]; if (o.tlsModern) tls.push('SSLProtocol -all +TLSv1.2 +TLSv1.3'); if (o.http2) { mods.add('http2'); tls.push('Protocols h2 http/1.1'); } out.push(...vhost(num(o.httpPort) || 80, o.redirectHttp ? [`Redirect permanent / https://${main}/`] : body), ''); out.push(...vhost(num(o.httpsPort) || 443, [...tls, '', ...body])); } else out.push(...vhost(num(o.httpPort) || 80, body)); return { text: `${out.join('\n')}\n`, modules: [...mods] }; } // ---------------------------------------------------------------- Caddy function caddy(o) { const domains = splitList(o.domains); const port = num(o.httpPort) || 80; const sport = num(o.httpsPort) || 443; // Ohne HTTPS: Adressen mit http:// (sonst holt Caddy für Domains automatisch Zertifikate) const addrs = (domains.length ? domains : [`:${port}`]).map((d) => { if (/^(https?:\/\/|:)/.test(d) || /:\d+$/.test(d)) return d; if (o.tls) return sport === 443 ? d : `${d}:${sport}`; return `http://${d}${port !== 80 ? `:${port}` : ''}`; }); const b = []; // Caddy verwaltet Zertifikate selbst; eigene Dateien nur auf ausdrücklichen Wunsch if (o.tls && o.certSource === 'custom') { const c = certPaths(o, domains[0] || 'example.com'); b.push(`tls ${c.cert} ${c.key}`); } if (o.gzip) b.push('encode zstd gzip'); const hdr = headerList(o); if (hdr.length || o.hideVersion) b.push('header {', ...hdr.map(([k, v]) => ` ${k} "${v.replace(/"/g, '\\"')}"`), ...(o.hideVersion ? [' -Server'] : []), '}'); if (num(o.maxBody)) b.push('request_body {', ` max_size ${num(o.maxBody)}MB`, '}'); const ips = splitList(o.allowIps); if (ips.length) b.push(`@blocked not remote_ip ${ips.join(' ')}`, 'respond @blocked 403'); if (o.accessLog && o.accessLog !== 'off') b.push('log {', ` output file ${o.accessLog}`, '}'); if (o.type === 'redirect') { b.push(`redir ${(o.redirectTo || 'https://example.com').replace(/\/$/, '')}${o.redirectPath ? '{uri}' : ''} ${o.redirectCode || 301}`); } else if (o.type === 'proxy') { const ups = splitList(o.upstreams); const p = o.proxyPath && o.proxyPath !== '/' ? `${o.proxyPath.replace(/\/?$/, '/')}* ` : ''; const sub = []; if (ups.length > 1) sub.push(`lb_policy ${o.lb === 'least_conn' ? 'least_conn' : o.lb === 'ip_hash' ? 'ip_hash' : 'round_robin'}`); if (!o.preserveHost) sub.push('header_up Host {upstream_hostport}'); if (!o.proxyBuffering) sub.push('flush_interval -1'); const https = ups.some((u) => /^https:/.test(u)); if ((num(o.proxyTimeout) && num(o.proxyTimeout) !== 60) || (https && o.upstreamInsecure)) { sub.push('transport http {', ...(num(o.proxyTimeout) && num(o.proxyTimeout) !== 60 ? [` read_timeout ${num(o.proxyTimeout)}s`] : []), ...(https && o.upstreamInsecure ? [' tls_insecure_skip_verify'] : []), '}'); } const target = (ups.length ? ups : ['127.0.0.1:3000']).map((u) => u.replace(/^http:\/\//, '')).join(' '); b.push(sub.length ? `reverse_proxy ${p}${target} {\n${indent(sub.join('\n'), 4)}\n}` : `reverse_proxy ${p}${target}`); } else { b.push(`root * ${o.root || `/var/www/${domains[0] || 'site'}`}`); if (o.denyHidden) b.push('@hidden path */.*', 'respond @hidden 404'); if (num(o.cacheDays)) b.push(`@assets path_regexp \\.(${ASSET_EXT})$`, `header @assets Cache-Control "public, max-age=${num(o.cacheDays) * 86400}"`); if (o.type === 'php') b.push(`php_fastcgi unix/${o.phpSocket || '/run/php/php-fpm.sock'}`); if (o.type === 'spa') b.push('try_files {path} /index.html'); b.push('file_server'); } if (o.extra?.trim()) b.push('# Custom', o.extra.trim()); return { text: `# Generated by MrTerm\n${addrs.join(', ')} {\n${indent(b.join('\n'), 4)}\n}\n`, modules: [] }; } // env: Eigenschaften des Zielhosts, z. B. { nginxVersion: '1.22.1' } const generate = (server, o, env = {}) => ({ nginx, apache, caddy }[server] || nginx)({ ...defaults(), ...o }, env); const api = { GROUPS, OPTIONS, PRESETS, defaults, visible, splitList, generate }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.WEBCONF = api; })(typeof window !== 'undefined' ? window : globalThis);