From c07639667bb9ae934748d2987089b17498c640e5 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Tue, 9 Jun 2026 10:30:33 +0000 Subject: [PATCH] =?UTF-8?q?Nginx=20proxy:=20local=20mode=20=E2=80=94=20Apa?= =?UTF-8?q?che=20port=20migration,=20one-click=20enable/disable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VhostManager: getApachePort() reads proxy_apache_port setting (default 80); writeApache() uses configured port; migrateApachePort() rewrites all vhosts and ports.conf; restoreApachePort() reverses the migration - ProxyManager::switchToLocalMode() — generator: installs nginx if needed, migrates Apache to 8090, configs nginx catch-all, starts nginx, syncs proxy hosts; rolls back Apache on nginx config failure - ProxyManager::disableLocalMode() — stops nginx, restores Apache to 80/443 - proxy.php: POST /api/proxy/switch-local and /api/proxy/disable-local (SSE stream) - admin.js: two-card "not configured" layout (Local Mode / Remote VM); proxySwitchLocal() modal with port picker + live progress stream; proxyDisableLocal() reverts with progress; 'Disable Local Mode' in service controls when mode=local Co-Authored-By: Claude Sonnet 4.6 --- panel/api/endpoints/proxy.php | 33 +++++++++ panel/lib/ProxyManager.php | 109 ++++++++++++++++++++++++++++++ panel/lib/VhostManager.php | 53 ++++++++++++++- panel/public/assets/js/admin.js | 116 ++++++++++++++++++++++++++++++-- 4 files changed, 303 insertions(+), 8 deletions(-) diff --git a/panel/api/endpoints/proxy.php b/panel/api/endpoints/proxy.php index 12ed122..138da27 100644 --- a/panel/api/endpoints/proxy.php +++ b/panel/api/endpoints/proxy.php @@ -141,6 +141,39 @@ try { $action === 'test-remote' && $method === 'POST' => Response::json(['success' => true, 'data' => ProxyManager::testRemote()]), + // POST switch-local — migrate Apache to internal port, install nginx, enable local proxy mode + ($action === 'switch-local') && $method === 'POST' => (function() use ($body) { + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('X-Accel-Buffering: no'); + ob_implicit_flush(true); + while (ob_get_level() > 0) ob_end_flush(); + $port = (int)($body['apache_port'] ?? 8090); + foreach (ProxyManager::switchToLocalMode($port) as $line) { + echo 'data: ' . json_encode(['line' => $line]) . "\n\n"; + flush(); + } + echo "data: " . json_encode(['done' => true]) . "\n\n"; + flush(); + exit; + })(), + + // POST disable-local — revert: Apache back to 80, stop nginx, disable proxy + ($action === 'disable-local') && $method === 'POST' => (function() { + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('X-Accel-Buffering: no'); + ob_implicit_flush(true); + while (ob_get_level() > 0) ob_end_flush(); + foreach (ProxyManager::disableLocalMode() as $line) { + echo 'data: ' . json_encode(['line' => $line]) . "\n\n"; + flush(); + } + echo "data: " . json_encode(['done' => true]) . "\n\n"; + flush(); + exit; + })(), + // POST setup-remote — run nginx setup on remote VM, stream output via SSE ($action === 'setup-remote') && $method === 'POST' => (function() { header('Content-Type: text/event-stream'); diff --git a/panel/lib/ProxyManager.php b/panel/lib/ProxyManager.php index 86d7e91..1885db1 100644 --- a/panel/lib/ProxyManager.php +++ b/panel/lib/ProxyManager.php @@ -326,6 +326,115 @@ class ProxyManager { return ['ok' => true, 'message' => 'Connected — ' . trim($out)]; } + // --- Local mode switch --- + + /** + * Switch to local proxy mode: + * Apache moves to $apachePort (default 8090), nginx takes 80/443. + * All existing vhosts are re-written; proxy hosts synced automatically. + * Yields progress lines suitable for SSE streaming. + */ + public static function switchToLocalMode(int $apachePort = 8090): \Generator { + require_once NOVACPX_LIB . '/VhostManager.php'; + $db = DB::getInstance(); + $save = function(string $k, string $v) use ($db) { + $db->execute("INSERT INTO settings (`key`, value) VALUES (?,?) ON DUPLICATE KEY UPDATE value=VALUES(value)", [$k, $v]); + }; + + yield "» Checking nginx installation...\n"; + if (!file_exists('/usr/sbin/nginx') && empty(shell_exec('which nginx 2>/dev/null'))) { + yield "» Installing nginx (apt-get install -y nginx)...\n"; + $out = shell_exec('apt-get update -qq 2>&1 && apt-get install -y nginx 2>&1'); + if ($out) yield trim($out) . "\n"; + if (!file_exists('/usr/sbin/nginx')) { yield "ERROR: nginx install failed. Aborting.\n"; return; } + yield " nginx installed\n"; + } else { + yield " nginx already installed\n"; + } + + yield "» Stopping nginx to avoid config conflicts...\n"; + shell_exec('systemctl stop nginx 2>/dev/null'); + + yield "» Migrating Apache from port 80 → {$apachePort}...\n"; + $changed = VhostManager::migrateApachePort(80, $apachePort); + yield " Updated {$changed} vhost(s) and ports.conf\n"; + + yield "» Restarting Apache on port {$apachePort}...\n"; + $apacheTest = shell_exec('apache2ctl configtest 2>&1'); + if (strpos($apacheTest ?? '', 'Syntax OK') === false) { + yield "ERROR: Apache config test failed:\n{$apacheTest}\nRolling back...\n"; + VhostManager::restoreApachePort($apachePort, 80); + shell_exec('systemctl restart apache2 2>/dev/null'); + yield " Apache restored to port 80. Aborting.\n"; + return; + } + shell_exec('systemctl restart apache2 2>/dev/null'); + yield " Apache is up on port {$apachePort}\n"; + + yield "» Configuring nginx (remove default site, add catch-all)...\n"; + @unlink('/etc/nginx/sites-enabled/default'); + $catchAll = "server {\n listen 80 default_server;\n server_name _;\n return 444;\n}\n"; + file_put_contents('/etc/nginx/sites-available/novacpx-default.conf', $catchAll); + if (!file_exists('/etc/nginx/sites-enabled/novacpx-default.conf')) { + @symlink('/etc/nginx/sites-available/novacpx-default.conf', '/etc/nginx/sites-enabled/novacpx-default.conf'); + } + if (!file_exists('/etc/nginx/conf.d/novacpx-proxy.conf')) { + file_put_contents('/etc/nginx/conf.d/novacpx-proxy.conf', + "client_max_body_size 256M;\nproxy_buffers 16 16k;\nproxy_buffer_size 16k;\n"); + } + + yield "» Saving proxy settings...\n"; + $save('proxy_mode', 'local'); + $save('proxy_backend_ip', '127.0.0.1'); + $save('proxy_apache_port', (string)$apachePort); + + yield "» Starting nginx on port 80/443...\n"; + shell_exec('systemctl enable nginx 2>/dev/null && systemctl start nginx 2>/dev/null'); + sleep(1); + if (!self::isRunning()) { + $err = shell_exec('nginx -t 2>&1'); + yield "ERROR: nginx failed to start:\n{$err}\n"; + yield "Apache is running on port {$apachePort}. Fix nginx config and try again.\n"; + return; + } + yield " nginx is running\n"; + + yield "» Syncing proxy hosts from all active accounts...\n"; + $added = self::syncFromAccounts(); + yield " Added {$added} proxy host(s)\n"; + self::writeAllConfigs(); + + yield "✓ Local proxy mode active!\n"; + yield " Apache: 127.0.0.1:{$apachePort} (PHP, file serving)\n"; + yield " Nginx: 0.0.0.0:80/443 (public, proxies to Apache)\n"; + yield " All accounts route through nginx → Apache automatically.\n"; + } + + /** + * Revert local mode: move Apache back to 80/443, stop nginx, disable proxy. + */ + public static function disableLocalMode(): \Generator { + require_once NOVACPX_LIB . '/VhostManager.php'; + $db = DB::getInstance(); + $apachePort = (int)($db->fetchOne("SELECT value FROM settings WHERE `key`='proxy_apache_port'")['value'] ?? 8090); + + yield "» Stopping nginx...\n"; + shell_exec('systemctl stop nginx 2>/dev/null && systemctl disable nginx 2>/dev/null'); + + yield "» Migrating Apache from port {$apachePort} → 80...\n"; + $changed = VhostManager::restoreApachePort($apachePort); + yield " Updated {$changed} vhost(s) and ports.conf\n"; + + yield "» Restarting Apache on port 80...\n"; + shell_exec('systemctl restart apache2 2>/dev/null'); + + yield "» Saving settings...\n"; + $db->execute("INSERT INTO settings (`key`, value) VALUES ('proxy_mode','disabled') ON DUPLICATE KEY UPDATE value='disabled'"); + $db->execute("UPDATE settings SET value='80' WHERE `key`='proxy_apache_port'"); + + yield "✓ Proxy disabled. Apache is back on port 80/443.\n"; + } + // --- Remote setup & uninstall --- public static function runSetupOnRemote(): \Generator { diff --git a/panel/lib/VhostManager.php b/panel/lib/VhostManager.php index 1986888..9333ab4 100644 --- a/panel/lib/VhostManager.php +++ b/panel/lib/VhostManager.php @@ -117,10 +117,61 @@ class VhostManager { @symlink($conf, "/etc/nginx/sites-enabled/novacpx-{$username}.conf"); } + // Returns the port Apache listens on for customer vhosts. + // 80 normally; changes to an internal port (e.g. 8090) in local proxy mode. + public static function getApachePort(): int { + $db = DB::getInstance(); + return (int)($db->fetchOne("SELECT value FROM settings WHERE `key`='proxy_apache_port'")['value'] ?? 80); + } + + // Re-write all existing novacpx-*.conf vhosts to use $to instead of $from, + // and update /etc/apache2/ports.conf accordingly. Returns count of files changed. + public static function migrateApachePort(int $from, int $to): int { + $count = 0; + foreach (glob('/etc/apache2/sites-available/novacpx-*.conf') ?: [] as $f) { + $orig = file_get_contents($f); + $updated = str_replace( + ["", "VirtualHost *:{$from}"], + ["", "VirtualHost *:{$to}"], + $orig + ); + if ($updated !== $orig) { file_put_contents($f, $updated); $count++; } + } + // Update ports.conf: swap Listen $from → Listen $to, drop Listen 443 (nginx handles SSL) + $ports = file_get_contents('/etc/apache2/ports.conf') ?: ''; + $ports = preg_replace('/^Listen\s+' . $from . '\b/m', "Listen {$to}", $ports); + $ports = preg_replace('/^Listen\s+443\b/m', '', $ports); + $ports = preg_replace('/.*?<\/IfModule>/s', '', $ports); + file_put_contents('/etc/apache2/ports.conf', $ports); + return $count; + } + + // Reverse migration: move Apache back from proxy port to standard 80/443. + public static function restoreApachePort(int $from, int $to = 80): int { + $count = 0; + foreach (glob('/etc/apache2/sites-available/novacpx-*.conf') ?: [] as $f) { + $orig = file_get_contents($f); + $updated = str_replace( + ["", "VirtualHost *:{$from}"], + ["", "VirtualHost *:{$to}"], + $orig + ); + if ($updated !== $orig) { file_put_contents($f, $updated); $count++; } + } + $ports = file_get_contents('/etc/apache2/ports.conf') ?: ''; + $ports = preg_replace('/^Listen\s+' . $from . '\b/m', "Listen {$to}", $ports); + if (!str_contains($ports, 'Listen 443')) { + $ports .= "\n\n Listen 443\n\n"; + } + file_put_contents('/etc/apache2/ports.conf', $ports); + return $count; + } + private static function writeApache(string $username, string $domain, string $docRoot, string $phpVer, string $logDir): void { + $port = self::getApachePort(); $sock = "/run/php/php{$phpVer}-fpm-{$username}.sock"; $conf = "/etc/apache2/sites-available/novacpx-{$username}.conf"; - file_put_contents($conf, " + file_put_contents($conf, " ServerName {$domain} ServerAlias www.{$domain} DocumentRoot {$docRoot} diff --git a/panel/public/assets/js/admin.js b/panel/public/assets/js/admin.js index 04d8c42..f340ebd 100644 --- a/panel/public/assets/js/admin.js +++ b/panel/public/assets/js/admin.js @@ -2651,13 +2651,23 @@ async function nginxProxyPage() { ${!inst ? ` -
- -

Nginx Proxy Not Active

-

Use a dedicated proxy VM (recommended) — run nginx on a separate LXC and control it from here via SSH. Or install nginx locally alongside Apache (requires moving Apache to port 8080).

-
- - +
+
+
+
🖥
+

Local Mode

+

nginx on this server. Apache moves to an internal port. All websites keep working — nginx proxies everything through. One-click setup.

+ +
+
+
🌐
+

Remote Proxy VM

+

Dedicated LXC or VM runs nginx. Panel pushes configs via SSH. Best for production — keeps proxy and hosting isolated.

+ +
+
+
+
` : ` @@ -2669,6 +2679,7 @@ ${!inst ? ` + ${cfg.mode === 'local' ? `` : ''}
@@ -2876,6 +2887,97 @@ window.proxySetupInstructions = async () => { `, null, { cancelLabel: 'Close', showConfirm: false }); }; +window.proxySwitchLocal = () => { + Nova.modal('Enable Local Nginx Proxy', ` +

Nginx will be installed on this server and take over ports 80/443. Apache moves to an internal port and keeps serving all PHP sites — end users see no change.

+
+ What will happen:
+ + 1. nginx installed (if not present)
+ 2. Apache moved from port 80 → 8090
+ 3. All existing vhosts updated
+ 4. nginx starts on port 80/443 and proxies to Apache
+ 5. Proxy hosts auto-synced from your accounts +
+
+
+ + +
+ `, () => { + const port = parseInt(document.getElementById('sl-port')?.value) || 8090; + const ov = Nova.modal('Switching to Local Proxy Mode', ` +

Moving Apache to port ${port} and starting nginx on 80/443…

+
Starting…\n
+ `, null, { cancelLabel: 'Close', showConfirm: false }); + + const log = document.getElementById('proxy-local-log'); + const es = new EventSource('/api/proxy/switch-local'); + let done = false; + + // POST with port — can't use native EventSource for POST, so use fetch+ReadableStream + es.close(); + fetch('/api/proxy/switch-local', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ apache_port: port }), + }).then(async res => { + const reader = res.body.getReader(); + const dec = new TextDecoder(); + let buf = ''; + while (true) { + const { value, done: d } = await reader.read(); + if (d) break; + buf += dec.decode(value, { stream: true }); + const parts = buf.split('\n\n'); + buf = parts.pop(); + for (const part of parts) { + const m = part.match(/^data: (.+)$/m); + if (!m) continue; + try { + const evt = JSON.parse(m[1]); + if (evt.line) { log.textContent += evt.line; log.scrollTop = log.scrollHeight; } + if (evt.done) { done = true; log.textContent += '\n— Done.\n'; setTimeout(() => Nova.loadPage('nginx-proxy', window._novaPages), 1500); } + } catch {} + } + } + }).catch(e => { log.textContent += '\n— Connection error: ' + e.message + '\n'; }); + + ov.querySelector('.modal-close')?.addEventListener('click', () => { done = true; }); + }, { confirmLabel: 'Switch Now' }); +}; + +window.proxyDisableLocal = () => { + Nova.confirm('Revert to direct Apache mode? nginx will be stopped and Apache will move back to port 80.', () => { + const ov = Nova.modal('Disabling Local Proxy Mode', ` +
Starting…\n
+ `, null, { cancelLabel: 'Close', showConfirm: false }); + const log = document.getElementById('proxy-disable-log'); + fetch('/api/proxy/disable-local', { method: 'POST', credentials: 'include' }).then(async res => { + const reader = res.body.getReader(); + const dec = new TextDecoder(); + let buf = ''; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + buf += dec.decode(value, { stream: true }); + const parts = buf.split('\n\n'); + buf = parts.pop(); + for (const part of parts) { + const m = part.match(/^data: (.+)$/m); + if (m) try { + const evt = JSON.parse(m[1]); + if (evt.line) { log.textContent += evt.line; log.scrollTop = log.scrollHeight; } + if (evt.done) setTimeout(() => Nova.loadPage('nginx-proxy', window._novaPages), 1000); + } catch {} + } + } + }); + }, true); +}; + window.proxyRunSetup = () => { const ov = Nova.modal('Setting Up Remote Nginx Proxy', `

Running setup on the remote proxy VM — this takes about 30 seconds.