ispconfig plugin

<?php
/*
    ISPConfig 3 - Custom plugin:
    reverse_proxy_mode_plugin

    Server-level only:
    - nginx_apache:
        * Enforce Apache loopback listens for backend/apps/ispconfig
        * Enable Apache RemoteIP (trust 127.0.0.1)
        * Write ONE nginx core reverse-proxy file: /etc/nginx/conf.d/reverse-proxy.conf
        * Reload services safely
        * Regenerate ISPConfig configs now
    - none:
        * Restore Apache ports.conf from ports.conf.orig if available, else remove managed block
        * Disable RemoteIP if enabled by us
        * Remove /etc/nginx/conf.d/reverse-proxy.conf
        * Reload services safely
*/

class reverse_proxy_mode_plugin {

    var $plugin_name = 'reverse_proxy_mode_plugin';
    var $class_name  = 'reverse_proxy_mode_plugin';

    public function onLoad() {
        global $app;
        $app->plugins->registerEvent('server_update', $this->plugin_name, 'server_update');
    }

    // ---------- config parser (server.config text key=value) ----------
    private function getCfgMap($server_id) {
        global $app;

        $server_id = intval($server_id);
        $rec = $app->db->queryOneRecord("SELECT config FROM server WHERE server_id = ?", $server_id);
        $cfg_text = isset($rec['config']) ? (string)$rec['config'] : '';

        $m = array();
        foreach(preg_split("/\r\n|\n|\r/", $cfg_text) as $line) {
            $line = trim($line);
            if($line === '' || $line[0] === '#' || $line[0] === ';') continue;
            if(substr($line, 0, 2) === '-[' && substr($line, -3) === ']-/') continue;

            $pos = strpos($line, '=');
            if($pos === false) continue;

            $k = trim(substr($line, 0, $pos));
            $v = trim(substr($line, $pos + 1));
            if($k !== '') $m[$k] = $v;
        }
        return $m;
    }

    private function getInt($m, $key, $default) {
        $v = isset($m[$key]) ? intval($m[$key]) : intval($default);
        if($v < 1) $v = intval($default);
        return $v;
    }

    private function sanitizePort($p, $fallback) {
        $p = intval($p);
        if($p < 1024 || $p > 65535) return intval($fallback);
        return $p;
    }
     
//     config parser (server.config text key=value) ----------
     
     
         // ---------- In 'nginx_apache' all website vhosts should bind to loopback backend port:  127.0.0.1:8080
     //------------In 'none' *:80 & *:443
     private function apacheRewriteVhostBind($mode, $backend_port) {
     
     @file_put_contents('/tmp/rp_rewrite.log', date('c')." mode=$mode port=$backend_port\n", FILE_APPEND);
     
    $dir = '/etc/apache2/sites-available';
    if(!is_dir($dir)) return;

    // Choose which files to rewrite.
    // ISPConfig website vhosts are commonly like: domain.vhost or 100-domain.vhost etc.
    // Adjust pattern if needed.
    $files = glob($dir . '/*.vhost');
    if(!is_array($files)) $files = [];

    foreach($files as $file) {
        if(!is_file($file)) continue;

        // Skip ISPConfig panel vhost and acme vhost (you already manage those separately)
        $base = basename($file);
        if(substr($base, -11) === '.rpnone.orig') continue;
        if(stripos($base, 'ispconfig') !== false) continue;
        if(stripos($base, 'acme') !== false) continue;

        $txt = @file_get_contents($file);
        if($txt === false) continue;

        $new = $txt;
        
        // This makes rollback deterministic and avoids regex guessing.
        if($mode !== 'nginx_apache') {
    if(is_file($file . '.rporig')) {
        @copy($file . '.rporig', $file);
        continue;
    }
}

       // Create a stable rollback backup ONLY when entering nginx_apache,
       // and only if it looks like a public vhost (contains *:80 or *:443).
/*
  1) Backup step (ONLY when switching into nginx_apache).
     Create a stable "public binds" backup once, only if file is still public.
*/
if($mode === 'nginx_apache') {
    if(!is_file($file . '.rporig')) {
        if(
            stripos($txt, '<VirtualHost *:80>') !== false ||
            stripos($txt, '<VirtualHost *:443>') !== false
        ) {
            @copy($file, $file . '.rporig');
        }
    }
}

/*
  2) Rewrite/restore step
*/
if($mode === 'nginx_apache') {

    $port = intval($backend_port);

    // Convert public vhosts to loopback bind
    $new = preg_replace('/<VirtualHost\s+\*\s*:\s*80\s*>/i',  '<VirtualHost 127.0.0.1:' . $port . '>', $new);
    $new = preg_replace('/<VirtualHost\s+\*\s*:\s*443\s*>/i', '<VirtualHost 127.0.0.1:' . $port . '>', $new);

} else { // mode === none

    // Best rollback: restore from stable backup if it exists
    if(is_file($file . '.rporig')) {
        @copy($file . '.rporig', $file);
        continue;
    }

    // Fallback rollback: rewrite loopback blocks back to *:80 / *:443
    $port = intval($backend_port);

    $new = preg_replace_callback(
        '/<VirtualHost\s+127\.0\.0\.1\s*:\s*' . $port . '\s*>(.*?)<\/VirtualHost>/is',
        function($m) use ($port) {
            $block = $m[0];
            $inner = $m[1];

            $is_ssl = (
                stripos($inner, 'SSLEngine on') !== false ||
                stripos($inner, 'SSLCertificateFile') !== false ||
                stripos($inner, 'SSLCertificateKeyFile') !== false
            );

            $target = $is_ssl ? '*:443' : '*:80';

            // Replace ONLY the opening tag of this block
            $block = preg_replace(
                '/^<VirtualHost\s+127\.0\.0\.1\s*:\s*' . $port . '\s*>/i',
                '<VirtualHost ' . $target . '>',
                $block,
                1
            );

            return $block;
        },
        $new
    );
}

// Write back only if changed
if($new !== $txt) {
    @file_put_contents($file, $new);
}

} // foreach

}
 

    private function detectIspconfigPort() {
        $candidates = array(
            '/etc/apache2/sites-available/ispconfig.vhost',
            '/etc/apache2/sites-available/000-ispconfig.conf',
            '/etc/apache2/sites-available/ispconfig.conf',
            '/etc/apache2/sites-enabled/ispconfig.vhost',
            '/etc/apache2/sites-enabled/000-ispconfig.conf',
            '/etc/apache2/sites-enabled/000-ispconfig.vhost',
        );

        foreach($candidates as $f) {
            if(!is_file($f)) continue;
            $txt = @file_get_contents($f);
            if($txt === false) continue;

            if(preg_match('/<VirtualHost\s+[^>]*:(\d+)\s*>/i', $txt, $m)) {
                $p = intval($m[1]);
                if($p > 0) return $p;
            }
            if(preg_match('/^\s*Listen\s+(\d+)\s*$/mi', $txt, $m)) {
                $p = intval($m[1]);
                if($p > 0) return $p;
            }
        }

        return 8080;
    }
    
    private function getIspconfigVhostFile() {
    $candidates = array(
        '/etc/apache2/sites-available/ispconfig.vhost',
        '/etc/apache2/sites-available/000-ispconfig.vhost',
        '/etc/apache2/sites-available/000-ispconfig.conf',
        '/etc/apache2/sites-enabled/ispconfig.vhost',
        '/etc/apache2/sites-enabled/000-ispconfig.vhost',
        '/etc/apache2/sites-enabled/000-ispconfig.conf',
    );
    foreach($candidates as $f) if(is_file($f)) return $f;
    return '';
}

    private function toggleListenInIspconfigVhost($comment_listen) {
//    $file = '/etc/apache2/sites-available/ispconfig.vhost';
//    if(!is_file($file)) return;
$file = $this->getIspconfigVhostFile();
if($file === '') return;

    // Backup once
    if(!is_file($file . '.orig')) {
        @copy($file, $file . '.orig');
    }

    $txt = @file_get_contents($file);
    if($txt === false) return;

    $lines = explode("\n", $txt);
    $changed = false;

    foreach($lines as &$line) {
        // Match: optional indentation, optional '#', then 'Listen', then rest of line
        // We only toggle leading comment marker for Listen directives.
        if(preg_match('/^(\s*)(#\s*)?Listen(\s+.*)$/', $line, $m)) {
            $indent = $m[1];
            $has_hash = isset($m[2]) && $m[2] !== '';
            $rest = $m[3];

            if($comment_listen) {
                if(!$has_hash) {
                    $line = $indent . '#Listen' . $rest;
                    $changed = true;
                }
            } else {
                if($has_hash) {
                    // remove only the first "#" before Listen
                    $line = $indent . 'Listen' . $rest;
                    $changed = true;
                }
            }
        }
    }
    unset($line);

    if($changed) {
        @file_put_contents($file, implode("\n", $lines) . "\n");
    }
}

    private function toggleApachePublicPortsInPortsConf($comment_public_ports) {
    $file  = '/etc/apache2/ports.conf';
    $begin = "# ISPConfig Reverse Proxy Mode BEGIN";
    $end   = "# ISPConfig Reverse Proxy Mode END";

    if(!is_file($file)) return;


    $txt = @file_get_contents($file);
    if($txt === false) return;

    $lines = explode("\n", $txt);
    $changed = false;
    $in_managed = false;

    foreach($lines as &$line) {
        if(strpos($line, $begin) !== false) { $in_managed = true; continue; }
        if(strpos($line, $end) !== false)   { $in_managed = false; continue; }

        // Never touch our managed block
        if($in_managed) continue;

        // Match Listen 80 / Listen 443 with optional leading "#"

        if(preg_match('/^(\s*)(#\s*)?Listen(\s+)(80|443)\s*$/', $line, $m)) {
            $indent   = $m[1];
            $has_hash = isset($m[2]) && $m[2] !== '';
            $spaces   = $m[3];
            $port     = $m[4];

            if($comment_public_ports) {
                if(!$has_hash) {
                    $line = $indent . '#Listen' . $spaces . $port;
                    $changed = true;
                }
            } else {
                if($has_hash) {
                    $line = $indent . 'Listen' . $spaces . $port;
                    $changed = true;
                }
            }
        }
    }
    unset($line);

    if($changed) {
        @file_put_contents($file, implode("\n", $lines) . "\n");
    }
}

    
    // Replace or append a managed block in a file
    private function writeManagedBlock($file, $begin, $end, $content) {
        $old = is_file($file) ? file_get_contents($file) : '';
        if($old === false) $old = '';

        $block = $begin . "\n" . $content . "\n" . $end;

        if(strpos($old, $begin) !== false && strpos($old, $end) !== false) {
            $pattern = '/' . preg_quote($begin, '/') . '.*?' . preg_quote($end, '/') . '/s';
            $new = preg_replace($pattern, $block, $old, 1);
        } else {
            $new = rtrim($old) . "\n\n" . $block . "\n";
        }

        if($new !== $old) {
            file_put_contents($file, $new);
        }
    }

    private function removeManagedBlock($file, $begin, $end) {
        if(!is_file($file)) return;

        $old = file_get_contents($file);
        if($old === false) return;

        if(strpos($old, $begin) === false || strpos($old, $end) === false) return;

        $pattern = '/' . preg_quote($begin, '/') . '.*?' . preg_quote($end, '/') . "\n?/s";
        $new = preg_replace($pattern, '', $old, 1);
        if($new !== $old) file_put_contents($file, trim($new) . "\n");
    }
    
    
    // Disable enabled symlinks Nginx
    private function nginxDisableIspconfigVhostsEnabled() {
    $enabled_dir = '/etc/nginx/sites-enabled';
    if(!is_dir($enabled_dir)) return;

    foreach(glob($enabled_dir . '/*-*.vhost') as $lnk) {
        if(is_link($lnk)) {
            @unlink($lnk);
        }
    }
}

     //Enable symlinks again
     private function nginxEnsureIspconfigVhostsEnabled($app) {
    $avail_dir   = '/etc/nginx/sites-available';
    $enabled_dir = '/etc/nginx/sites-enabled';
    if(!is_dir($avail_dir) || !is_dir($enabled_dir)) return;

    // We only enable ISPConfig vhost files: *.vhost
    $files = glob($avail_dir . '/*.vhost');
    if(!is_array($files)) return;

    foreach($files as $src) {
        if(!is_file($src)) continue;

        $base = basename($src);

        // Create matching enabled link:
        // If your system uses 100-<domain>.vhost naming, preserve that.
        // Example: sites-available/micino.site.vhost -> sites-enabled/100-micino.site.vhost
        // If it's already prefixed (100- / 900-), don't double-prefix.
        if(preg_match('/^\d+-/', $base)) {
            $dst = $enabled_dir . '/' . $base;
        } else {
            $dst = $enabled_dir . '/100-' . $base;
        }

        if(is_link($dst) || is_file($dst)) continue; // already enabled

        // Ensure link target is absolute
        $ok = @symlink($src, $dst);
        if(!$ok) {
            $app->log("WARN: failed to symlink nginx vhost: $dst -> $src", LOGLEVEL_WARN);
        }
    }
}

     
    

    private function safeReloadServices($app) {
        $app->system->exec_safe("apache2ctl configtest >/dev/null 2>&1 && systemctl reload apache2 || systemctl restart apache2");
        $app->system->exec_safe("nginx -t >/dev/null 2>&1 && systemctl reload nginx || systemctl restart nginx");
    }

    // ---------- main handler ----------
    public function server_update($event_name, $data) {
        global $app;

        $server_id = intval($data['new']['server_id']);
        if($server_id <= 0) return;

        $cfg  = $this->getCfgMap($server_id);
        $mode = isset($cfg['reverse_proxy_mode']) ? $cfg['reverse_proxy_mode'] : 'none';
        
        //Now $backend_port exists in BOTH branches.
        $backend_port = $this->sanitizePort($this->getInt($cfg, 'apache_backend_port', 8080), 8080);


        // ----------------------------
        // ROLLBACK branch: mode == none
        // ----------------------------
        if($mode === 'none') {

            // 1) Restore Apache ports.conf if we saved an original
            if(is_file('/etc/apache2/ports.conf.orig')) {
                @copy('/etc/apache2/ports.conf.orig', '/etc/apache2/ports.conf');
            } else {
                // If no orig, remove only our managed block
                $this->removeManagedBlock(
                    '/etc/apache2/ports.conf',
                    "# ISPConfig Reverse Proxy Mode BEGIN",
                    "# ISPConfig Reverse Proxy Mode END"
                );
            }
            // Restore public ports Listen directives (if not restoring full ports.conf.orig)
            $this->toggleApachePublicPortsInPortsConf(false);


            // 2) Disable RemoteIP (only what we enabled)
            // We leave the file in place; disabling conf+module is enough.
            $app->system->exec_safe("a2disconf remoteip >/dev/null 2>&1 || true");
            $app->system->exec_safe("a2dismod remoteip >/dev/null 2>&1 || true");
            
            @unlink('/etc/apache2/conf-available/remoteip.conf');

            // 3) Remove nginx core reverse proxy file
            $ng_file = '/etc/nginx/conf.d/reverse-proxy.conf';
            if(is_file($ng_file)) @unlink($ng_file);
            
            // 3+) Ensure ISPConfig panel vhost Listen directives are enabled in "none" mode
             $this->toggleListenInIspconfigVhost(false);
             
             //3++) Disable ISPConfig nginx site vhosts (so nginx stops binding 80/443
              $this->nginxDisableIspconfigVhostsEnabled();

            // 4) Regenerate ISPConfig configs (optional but helpful)
            $app->system->exec_safe("/usr/local/ispconfig/server/server.sh --force >/dev/null 2>&1 || true");
            
            // 5) rewrite apache website vhosts back to public binds
            $this->apacheRewriteVhostBind('none', $backend_port);

            // 6) Reload services
            $this->safeReloadServices($app);
            

            return;
        }

        // ----------------------------
        // APPLY branch: mode == nginx_apache
        // ----------------------------
        if($mode !== 'nginx_apache') {
            // haproxy_apache not implemented here
            return;
        }

        // Ports (from server.config)
        $backend_port = $this->sanitizePort($this->getInt($cfg, 'apache_backend_port', 8080), 8080);

        $apps_enabled = (isset($cfg['apps_vhost_enabled']) && $cfg['apps_vhost_enabled'] === 'y');
        $apps_port    = $this->sanitizePort($this->getInt($cfg, 'apps_vhost_port', 8081), 8081);

        $ispconfig_port = 0;
        if(isset($cfg['apache_ispconfig_port'])) $ispconfig_port = intval($cfg['apache_ispconfig_port']);
        if($ispconfig_port <= 0) $ispconfig_port = $this->detectIspconfigPort();
        $ispconfig_port = $this->sanitizePort($ispconfig_port, 8086);
        
        // Disable Listen directives in ISPConfig panel vhost when nginx is in front
         $this->toggleListenInIspconfigVhost(true);
         

        // ---------- 1) Apache loopback listens ----------
        if(is_file('/etc/apache2/ports.conf') && !is_file('/etc/apache2/ports.conf.orig')) {
            @copy('/etc/apache2/ports.conf', '/etc/apache2/ports.conf.orig');
        }
        
        // In reverse proxy mode, Apache must NOT bind public 80/443
         $this->toggleApachePublicPortsInPortsConf(true);

        $listens = array();
        $listens[] = "Listen 127.0.0.1:" . $backend_port;
        if($apps_enabled) $listens[] = "Listen 127.0.0.1:" . $apps_port;
        $listens[] = "Listen 127.0.0.1:" . $ispconfig_port;
        $listens = array_values(array_unique($listens));

        $this->writeManagedBlock(
            '/etc/apache2/ports.conf',
            "# ISPConfig Reverse Proxy Mode BEGIN",
            "# ISPConfig Reverse Proxy Mode END",
            implode("\n", $listens)
        );

        // ---------- 2) Apache RemoteIP ----------
        $remoteip_conf = "RemoteIPHeader X-Forwarded-For\nRemoteIPTrustedProxy 127.0.0.1\n";
        @file_put_contents('/etc/apache2/conf-available/remoteip.conf', $remoteip_conf);

        $app->system->exec_safe("a2enmod remoteip >/dev/null 2>&1 || true");
        $app->system->exec_safe("a2enconf remoteip >/dev/null 2>&1 || true");

        // ---------- 3) Nginx core reverse proxy config (exactly as requested) ----------
        $ng_file = '/etc/nginx/conf.d/reverse-proxy.conf';

        $ssl_crt = '/etc/ssl/certs/nginx-selfsigned.crt';
        $ssl_key = '/etc/ssl/private/nginx-selfsigned.key';

        $ng = "";
        $ng .= "server {\n";
        $ng .= "    listen 80 default_server;\n";
        $ng .= "    listen [::]:80 default_server;\n";
        $ng .= "    server_name _;\n\n";
        $ng .= "    include /etc/nginx/snippets/ispconfig-acme.conf;\n\n";
        $ng .= "    location / {\n";
        $ng .= "        proxy_pass http://127.0.0.1:$backend_port;\n";
        $ng .= "        proxy_set_header Host \$host;\n";
        $ng .= "        proxy_set_header X-Real-IP \$remote_addr;\n";
        $ng .= "        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\n";
        $ng .= "        proxy_set_header X-Forwarded-Proto \$scheme;\n";
        $ng .= "        proxy_hide_header Upgrade;\n";
        $ng .= "    }\n";
        $ng .= "}\n\n";

        $ng .= "server {\n";
        $ng .= "    listen 443 ssl http2 default_server;\n";
        $ng .= "    listen [::]:443 ssl http2 default_server;\n";
        $ng .= "    server_name _;\n\n";
        $ng .= "    ssl_certificate  $ssl_crt;\n";
        $ng .= "    ssl_certificate_key $ssl_key;\n\n";
        $ng .= "    ssl_protocols TLSv1.2 TLSv1.3;\n\n";

        $ng .= "    # optional: make /ispconfig redirect to /ispconfig/\n";
        $ng .= "    location = /ispconfig {\n";
        $ng .= "        return 301 /ispconfig/;\n";
        $ng .= "    }\n\n";
        $ng .= "    location = /ispconfig/index.php {\n";
        $ng .= "        return 301 /ispconfig/;\n";
        $ng .= "    }\n\n";

        $ng .= "    # ISPConfig assets requested without /ispconfig prefix (fixes favicon/theme assets)\n";
        $ng .= "    location ^~ /themes/ {\n";
        $ng .= "        proxy_pass https://127.0.0.1:$ispconfig_port/themes/;\n";
        $ng .= "        proxy_set_header Host \$host;\n";
        $ng .= "        proxy_ssl_verify off;\n";
        $ng .= "    }\n\n";

        $ng .= "    location ^~ /ispconfig/ {\n";
        $ng .= "        proxy_pass https://127.0.0.1:$ispconfig_port/;\n";
        $ng .= "        proxy_set_header Host \$host;\n";
        $ng .= "        proxy_set_header X-Real-IP \$remote_addr;\n";
        $ng .= "        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\n";
        $ng .= "        proxy_set_header X-Forwarded-Proto \$scheme;\n";
        $ng .= "        proxy_ssl_verify off;\n\n";
        $ng .= "        # fix backend redirects like Location: /login/\n";
        $ng .= "        proxy_redirect ~^(/.*)\$ /ispconfig\$1;\n";
        $ng .= "    }\n\n";

        $ng .= "    location / {\n";
        $ng .= "        proxy_pass http://127.0.0.1:$backend_port;\n";
        $ng .= "        proxy_set_header Host \$host;\n";
        $ng .= "        proxy_set_header X-Real-IP \$remote_addr;\n";
        $ng .= "        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\n";
        $ng .= "        proxy_set_header X-Forwarded-Proto \$scheme;\n";
        $ng .= "        proxy_hide_header Upgrade;\n";
        $ng .= "    }\n";
        $ng .= "}\n";

        @file_put_contents($ng_file, $ng);

        // ---------- 3++) Ensure ISPConfig nginx vhost links are enabled ----------
        $this->nginxEnsureIspconfigVhostsEnabled($app);

        // ---------- 4) Regenerate ISPConfig configs now ----------
        $app->system->exec_safe("/usr/local/ispconfig/server/server.sh --force >/dev/null 2>&1 || true");
        
        // ---------- 5) rewrite apache website vhosts to loopback
        $this->apacheRewriteVhostBind('nginx_apache', $backend_port);

        // ---------- 6) Reload services safely ----------
        $this->safeReloadServices($app);
    }
}

 

2026안정적인 CORE