HEX
Server: Apache
System: Linux zacp120.webway.host 4.18.0-553.50.1.lve.el8.x86_64 #1 SMP Thu Apr 17 19:10:24 UTC 2025 x86_64
User: govancoz (1003)
PHP: 8.3.26
Disabled: exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Upload Files
File: //opt/cpguard/app/scripts/csf_migration.php
#!/opt/cpguard/cpg-php-fpm/bin/php
<?php
require dirname(__DIR__) . '/vendor/autoload.php';
use cPGuard\Core\Firewall\Firewall;
use Helpers\IP;

// ANSI color codes for better CLI display
const COLORS = [
    'RED' => "\e[31m",
    'GREEN' => "\e[32m",
    'YELLOW' => "\e[33m",
    'BLUE' => "\e[34m",
    'MAGENTA' => "\e[35m",
    'CYAN' => "\e[36m",
    'WHITE' => "\e[37m",
    'BOLD' => "\e[1m",
    'RESET' => "\e[0m"
];

class CSFMigrator
{
    private $errors = [];
    private $warnings = [];
    private $migrationData = [];

    public function run()
    {
        // Simple one-line header
        echo COLORS['CYAN'] . COLORS['BOLD'] . "CSF to cPGuard Migration Tool" . COLORS['RESET'] . "\n\n";

        try {
            // Quick pre-flight checks
            if (!$this->performQuickChecks()) {
                $this->displayErrors();
                exit(1);
            }

            // Analysis phase with simple progress
            echo COLORS['CYAN'] . "Analyzing CSF files and configuration..." . COLORS['RESET'] . "";
            $this->prepareData();

            if (!empty($this->errors)) {
                echo COLORS['RED'] . "\nAnalysis failed:" . COLORS['RESET'] . "\n";
                $this->displayErrors();
                exit(1);
            }
            sleep(1);

            // Display summary
            echo "\r\033[K\033[36m" . "MIGRATION SUMMARY" . COLORS['RESET'] . "\033[0m\n\n";
            $this->displaySummary();

            // Confirm migration
            if (!$this->confirmMigration()) {
                echo COLORS['YELLOW'] . "Migration cancelled." . COLORS['RESET'] . "\n";
                exit(0);
            }

            // Execute migration
            echo "Executing migration..." . COLORS['RESET'] . "";
            $this->executeMigration();
            sleep(1);

            // Complete
            $this->displayCompletion();

        } catch (Exception $e) {
            echo COLORS['RED'] . "Fatal error: " . $e->getMessage() . COLORS['RESET'] . "\n";
            exit(1);
        }
    }

    private function performQuickChecks()
    {
        if (!file_exists('/etc/csf/csf.conf')) {
            $this->errors[] = "CSF is not installed on this server";
            return false;
        }

        if (!is_writable('/opt/cpguard')) {
            $this->errors[] = "cPGuard directory is not writable";
            return false;
        }

        if (!class_exists('Helpers\IP')) {
            $this->errors[] = "Required IP helper class not found";
            return false;
        }

        return true;
    }

    private function prepareData()
    {
        $existingWhitelistIPs = $this->getExistingIPs('/opt/cpguard/whitelistips.txt');

        // Prepare csf.allow
        $this->migrationData['csf_allow'] = $this->prepareIPsFromFile('/etc/csf/csf.allow', $existingWhitelistIPs, 'csf.allow');

        // Prepare csf.ignore
        $this->migrationData['csf_ignore'] = $this->prepareIPsFromFile('/etc/csf/csf.ignore', $existingWhitelistIPs, 'csf.ignore');

        // Prepare blacklist IPs
        $this->migrationData['blacklist'] = $this->prepareBlacklistIPs();

        // Prepare configuration
        $this->migrationData['config'] = $this->prepareConfigData();

        // Check for country code conflicts
        $this->checkCountryCodeConflicts();

        // Combine whitelist data for migration
        $this->migrationData['whitelist_combined'] = $this->combineWhitelistData();

        $this->validatePreparedData();
    }

    private function checkCountryCodeConflicts()
    {
        $allowCountries = $this->migrationData['config']['CC_ALLOW']['valid'] ?? [];
        $denyCountries = $this->migrationData['config']['CC_DENY']['valid'] ?? [];

        if (empty($allowCountries) || empty($denyCountries)) {
            return; // No conflict possible if one list is empty
        }

        $conflicts = array_intersect($allowCountries, $denyCountries);

        if (!empty($conflicts)) {
            $conflictList = implode(', ', $conflicts);
            $this->errors[] = "Country code conflict detected: The following countries appear in both CC_ALLOW and CC_DENY: $conflictList";
            $this->errors[] = "Please resolve this conflict in CSF configuration before migration";
        }
    }

    private function prepareIPsFromFile($filename, $existingIPs, $sourceName)
    {
        if (!file_exists($filename)) {
            return [
                'source' => $sourceName,
                'file_exists' => false,
                'new_ips' => [],
                'valid_lines' => [],
                'invalid' => [],
                'duplicates' => [],
                'already_exists' => [],
                'total_lines' => 0
            ];
        }

        $lines = file($filename, FILE_IGNORE_NEW_LINES);
        $totalLines = count($lines);

        $newIPs = [];
        $validLines = [];
        $invalidIPs = [];
        $duplicatesInFile = [];
        $alreadyExists = [];
        $seenIPs = [];
        $v6_count = 0;
        $v4_count = 0;

        foreach ($lines as $line) {
            $originalLine = trim($line);


            // Extract IP and comment
            $parts = explode('#', $originalLine, 2);
            $ipRange = trim($parts[0]);
            $comment = isset($parts[1]) ? trim($parts[1]) : '';

            if (empty($ipRange)) {
                continue;
            }

            // Validate IP
            if (!IP::validateRange($ipRange)) {
                $invalidIPs[] = $ipRange;
                continue;
            }

            // Check for duplicates within this file
            if (in_array($ipRange, $seenIPs)) {
                $duplicatesInFile[] = $ipRange;
                continue;
            }
            $seenIPs[] = $ipRange;

            // Check if IP already exists in cPGuard
            if (in_array($ipRange, $existingIPs)) {
                $alreadyExists[] = $ipRange;
                continue;
            }

            // Create formatted line with comment handling
            $formattedLine = $this->formatIPLine($ipRange, $comment, $sourceName);

            $newIPs[] = $ipRange;
            str_contains($ipRange, ':') ? $v6_count++ : $v4_count++;

            $validLines[] = $formattedLine;
        }

        return [
            'source' => $sourceName,
            'file_exists' => true,
            'new_ips' => $newIPs,
            'valid_lines' => $validLines,
            'invalid' => $invalidIPs,
            'duplicates' => $duplicatesInFile,
            'already_exists' => $alreadyExists,
            'total_lines' => $totalLines,
            'v4_count' => $v4_count,
            'v6_count' => $v6_count
        ];
    }

    private function formatIPLine($ipRange, $comment, $sourceName)
    {
        if (!empty($comment)) {
            // Keep original comment
            return "$ipRange # $comment";
        } else {
            // Add source information as comment
            return "$ipRange # from $sourceName";
        }
    }

    private function prepareBlacklistIPs()
    {
        $denyFile = '/etc/csf/csf.deny';
        $existingBlacklistIPs = $this->getExistingIPs('/opt/cpguard/blacklistips.txt');

        return $this->prepareIPsFromFile($denyFile, $existingBlacklistIPs, 'csf.deny');
    }

    private function combineWhitelistData()
    {
        $combinedValidLines = array_merge(
            $this->migrationData['csf_allow']['valid_lines'],
            $this->migrationData['csf_ignore']['valid_lines']
        );

        $combinedNewIPs = array_merge(
            $this->migrationData['csf_allow']['new_ips'],
            $this->migrationData['csf_ignore']['new_ips']
        );

        return [
            'valid_lines' => $combinedValidLines,
            'new_ips' => $combinedNewIPs,
            'total_new' => count($combinedNewIPs)
        ];
    }

    private function prepareConfigData()
    {
        $csfConfig = $this->fetchCSFConfig();
        $configMapping = [
            'TCP_IN' => 'fw_ports_tcp_in',
            'TCP_OUT' => 'fw_ports_tcp_out',
            'UDP_IN' => 'fw_ports_udp_in',
            'UDP_OUT' => 'fw_ports_udp_out',
            'CC_DENY' => 'fw_blacklist_country',
            'CC_ALLOW' => 'fw_whitelist_country'
        ];

        $preparedConfig = [];

        foreach ($configMapping as $csfKey => $cpguardKey) {
            $data = $csfConfig[$csfKey] ?? [];
            $validData = [];
            $invalidData = [];

            if (in_array($csfKey, ['TCP_IN', 'TCP_OUT', 'UDP_IN', 'UDP_OUT'])) {
                foreach ($data as $port) {
                    $port = trim($port);
                    if (empty($port))
                        continue;

                    $validatedPort = $this->validatePort($port);
                    if ($validatedPort !== null) {
                        $validData[] = $validatedPort;
                    } else {
                        $invalidData[] = $port;
                    }
                }
                if (!empty($invalidData)) {
                    $this->errors[] = "Invalid port detected: The following ports are invalid: " . implode(',', $invalidData);
                    $this->errors[] = "Please resolve this conflict in CSF configuration before migration";
                    return;
                }
            } else {
                foreach ($data as $countryCode) {
                    $countryCode = trim($countryCode);
                    if (empty($countryCode))
                        continue;

                    if ($this->countryExists($countryCode)) {
                        $validData[] = strtoupper($countryCode);
                    } else {
                        $invalidData[] = $countryCode;
                    }
                }
            }

            $preparedConfig[$csfKey] = [
                'cpguard_key' => $cpguardKey,
                'valid' => $validData,
                'invalid' => $invalidData,
                'original_values' => $data
            ];
        }

        return $preparedConfig;
    }

    private function validatePreparedData()
    {
        $hasData = false;

        if (!empty($this->migrationData['whitelist_combined']['new_ips']))
            $hasData = true;
        if (!empty($this->migrationData['blacklist']['new_ips']))
            $hasData = true;
        if (!empty($this->migrationData['config'])) {
            foreach ($this->migrationData['config'] as $config) {
                if (!empty($config['valid']))
                    $hasData = true;
            }
        }
        if (!$hasData) {
            $this->warnings[] = "No new data found to migrate";
        }
    }

    private function displaySummary()
    {
        // IP Files Summary
        $this->displayIPFileSummary('Whitelist IPs (csf.allow)', $this->migrationData['csf_allow']);
        $this->displayIPFileSummary('Whitelist IPs (csf.ignore)', $this->migrationData['csf_ignore']);

        // Combined whitelist summary
        $totalNew = $this->migrationData['whitelist_combined']['total_new'];
        if ($totalNew > 0) {
            echo COLORS['GREEN'] . COLORS['BOLD'] . "└─ Combined: $totalNew whitelist IPs will be imported" . COLORS['RESET'] . "\n\n";
        } else {
            echo COLORS['YELLOW'] . "└─ Combined: No new whitelist IPs to import" . COLORS['RESET'] . "\n\n";
        }

        $this->displayIPFileSummary('Blacklist IPs (csf.deny)', $this->migrationData['blacklist']);

        // Configuration Summary
        echo COLORS['BOLD'] . "CONFIGURATION SETTINGS" . COLORS['RESET'] . "\n\n";
        foreach ($this->migrationData['config'] as $key => $config) {
            $validCount = count($config['valid']);
            $invalidCount = count($config['invalid']);
            $originalCount = count($config['original_values']);

            if ($originalCount == 0) {
                echo COLORS['YELLOW'] . "  $key: No configuration found" . COLORS['RESET'] . "\n";
                continue;
            }

            echo COLORS['BOLD'] . "  $key:" . COLORS['RESET'] . "\n";

            if ($validCount > 0) {
                echo "    " . COLORS['GREEN'] . "✓ Valid ($validCount): " . COLORS['RESET'];
                $this->displayWrappedValues($config['valid'], 4);
            }

            if ($invalidCount > 0) {
                echo "    " . COLORS['RED'] . "✗ Invalid ($invalidCount): " . COLORS['RESET'];
                $this->displayWrappedValues($config['invalid'], 4);
            }

            echo "\n";
        }

        // Display warnings if any
        if (!empty($this->warnings)) {
            echo COLORS['YELLOW'] . COLORS['BOLD'] . "WARNINGS" . COLORS['RESET'] . "\n\n";
            foreach ($this->warnings as $warning) {
                echo "  • " . $warning . "\n";
            }
            echo "\n";
        }
    }

    private function displayIPFileSummary($title, $data)
    {
        echo COLORS['BOLD'] . $title . ":" . COLORS['RESET'] . "\n";

        if (!$data['file_exists']) {
            echo "  " . COLORS['YELLOW'] . "File not found" . COLORS['RESET'] . "\n\n";
            return;
        }

        $newCount = count($data['new_ips']);
        $duplicateCount = count($data['duplicates']);
        $invalidCount = count($data['invalid']);
        $alreadyExistsCount = count($data['already_exists']);

        printf(
            "  %-15s %s%-3d%s will be imported\n",
            'New IPs:',
            $newCount > 0 ? COLORS['GREEN'] : COLORS['YELLOW'],
            $newCount,
            COLORS['RESET']
        );

        printf(
            "  %-15s %s%-3d%s ips\n",
            'IP v4:',
            COLORS['RED'],
            $data['v4_count'],
            COLORS['RESET']
        );

        printf(
            "  %-15s %s%-3d%s ips\n",
            'IP v6:',
            COLORS['RED'],
            $data['v6_count'],
            COLORS['RESET']
        );

        printf("  %-15s %-3d already exist\n", 'Existing IPs:', $alreadyExistsCount);

        if ($duplicateCount > 0) {
            printf(
                "  %-15s %s%-3d%s duplicates skipped\n",
                'Duplicates:',
                COLORS['YELLOW'],
                $duplicateCount,
                COLORS['RESET']
            );
        }

        if ($invalidCount > 0) {
            printf(
                "  %-15s %s%-3d%s comment/unsupported entries\n",
                'Skipped lines:',
                COLORS['RED'],
                $invalidCount,
                COLORS['RESET']
            );
        }

        echo "\n";
    }

    private function displayWrappedValues($values, $indent = 0)
    {
        $indentStr = str_repeat(' ', $indent);
        $lineLength = 80 - $indent;
        $currentLine = '';

        foreach ($values as $i => $value) {
            $separator = ($i < count($values) - 1) ? ', ' : '';
            $addition = $value . $separator;

            if (strlen($currentLine . $addition) > $lineLength && !empty($currentLine)) {
                echo $currentLine . "\n" . $indentStr;
                $currentLine = $addition;
            } else {
                $currentLine .= $addition;
            }
        }

        if (!empty($currentLine)) {
            echo $currentLine . "\n";
        }
    }

    private function confirmMigration()
    {
        echo COLORS['YELLOW'] . COLORS['BOLD'] . "Proceed with migration? [y/N]: " . COLORS['RESET'];
        $handle = fopen("php://stdin", "r");
        $line = fgets($handle);
        fclose($handle);

        return trim(strtolower($line)) === 'y' || trim(strtolower($line)) === 'yes';
    }

    private function executeMigration()
    {
        $steps = [];

        if (!empty($this->migrationData['whitelist_combined']['valid_lines'])) {
            $count = $this->migrationData['whitelist_combined']['total_new'];
            $steps[] = ['type' => 'whitelist', 'count' => $count, 'desc' => "whitelist IPs"];
        }

        if (!empty($this->migrationData['blacklist']['valid_lines'])) {
            $count = count($this->migrationData['blacklist']['new_ips']);
            $steps[] = ['type' => 'blacklist', 'count' => $count, 'desc' => "blacklist IPs"];
        }

        foreach ($this->migrationData['config'] as $key => $config) {
            if (!empty($config['valid'])) {
                $count = count($config['valid']);
                $steps[] = ['type' => 'config', 'key' => $key, 'config' => $config, 'count' => $count, 'desc' => $key];
            }
        }

        foreach ($steps as $i => $step) {
            switch ($step['type']) {
                case 'whitelist':
                    $this->migrateIPs($this->migrationData['whitelist_combined']['valid_lines'], '/opt/cpguard/whitelistips.txt');
                    break;
                case 'blacklist':
                    $this->migrateIPs($this->migrationData['blacklist']['valid_lines'], '/opt/cpguard/blacklistips.txt');
                    break;
                case 'config':
                    $this->updateConfig($step['config']['cpguard_key'], $step['config']['valid']);
                    break;
            }
        }
    }

    private function displayCompletion()
    {
        echo "\r\033[K\033\n[32m✓ Migration Completed Successfully!\033[0m\n\n";

        // Migration results
        $whitelistTotal = $this->migrationData['whitelist_combined']['total_new'];
        $blacklistTotal = count($this->migrationData['blacklist']['new_ips']);

        if ($whitelistTotal > 0 || $blacklistTotal > 0) {
            echo COLORS['BOLD'] . "Migration Results:" . COLORS['RESET'] . "\n";

            if ($whitelistTotal > 0) {
                echo "  • $whitelistTotal whitelist IPs imported\n";
            }

            if ($blacklistTotal > 0) {
                echo "  • $blacklistTotal blacklist IPs imported\n";
            }

            foreach ($this->migrationData['config'] as $key => $config) {
                $count = count($config['valid']);
                if ($count > 0) {
                    echo "  • $key: $count items imported\n";
                }
            }
            echo "\n";
        }

        $this->fw_restart();
    }

    private function displayErrors()
    {
        if (!empty($this->errors)) {
            foreach ($this->errors as $error) {
                echo "  " . COLORS['RED'] . "✗ " . $error . COLORS['RESET'] . "\n";
            }
        }
    }

    // Helper methods
    private function getExistingIPs($file)
    {
        if (!file_exists($file)) {
            return [];
        }

        $ips = [];
        foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
            if (preg_match('/\s*([0-9a-fA-F:.\/\-]+)[\s#]*(.*)$/m', $line, $matches)) {
                $ips[] = trim($matches[1]);
            }
        }
        return $ips;
    }

    private function fetchCSFConfig()
    {
        $data = [];
        $fileCsfConf = '/etc/csf/csf.conf';

        if (!file_exists($fileCsfConf)) {
            return $data;
        }

        $csfConf = file_get_contents($fileCsfConf);
        $patterns = [
            'TCP_IN' => '/^\s*TCP_IN\s*\=\s*\"([^\"]*)\"/m',
            'UDP_IN' => '/^\s*UDP_IN\s*\=\s*\"([^\"]*)\"/m',
            'TCP_OUT' => '/^\s*TCP_OUT\s*\=\s*\"([^\"]*)\"/m',
            'UDP_OUT' => '/^\s*UDP_OUT\s*\=\s*\"([^\"]*)\"/m',
            'CC_DENY' => '/^\s*CC_DENY\s*\=\s*\"([^\"]*)\"/m',
            'CC_ALLOW' => '/^\s*CC_ALLOW\s*\=\s*\"([^\"]*)\"/m'
        ];

        foreach ($patterns as $key => $pattern) {
            if (preg_match($pattern, $csfConf, $matches)) {
                $data[$key] = array_filter(array_map('trim', explode(',', $matches[1])));
            }
        }

        return $data;
    }

    private function validatePort($port)
    {
        if (empty($port)) {
            return null;
        }
        if (strpos($port, ':') !== false) {
            $port = str_replace(':', '-', $port);
        }
        if (strpos($port, '-') !== false) {
            $range = explode('-', $port);
            if (count($range) !== 2) {
                return null;
            }

            $start = (int) trim($range[0]);
            $end = (int) trim($range[1]);

            if ($start < 1 || $start > 65535 || $end < 1 || $end > 65535 || $start > $end) {
                return null;
            }

            return "$start-$end";
        } else {
            if (!is_numeric($port) || $port < 1 || $port > 65535) {
                return null;
            }
            return (string) (int) $port;
        }
    }

    private function countryExists($countryCode)
    {
        $geoDir = ROOT . '/resources/geoip/';
        return file_exists($geoDir . strtolower($countryCode) . '.zone');
    }

    private function migrateIPs($lines, $file)
    {
        if (!empty($lines)) {
            file_put_contents($file, implode(PHP_EOL, $lines) . PHP_EOL, FILE_APPEND | LOCK_EX);
        }
    }

    private function updateConfig($key, $values)
    {
        config($key, $values);
    }
    private function fw_restart()
    {
        $fw = new Firewall();
        $fw->disable();

        if (!config('fw_switch')) {
            echo "Firewall is disabled. Changes not applied\n";
            return false;
        }

        echo "Restarting firewall... This may take a few seconds...";


        if ($fw->enable()) {
            echo "\r\e[KFirewall \e[32mrestarted\e[0m" . PHP_EOL;
        } else {
            echo "\r\e[K\e[31mFirewall restart failed\e[0m" . PHP_EOL;
        }
    }
}

// Main execution
if (!file_exists('/etc/csf/csf.conf')) {
    echo COLORS['RED'] . "CSF is not installed on this server" . COLORS['RESET'] . "\n";
    exit(1);
}

$migrator = new CSFMigrator();
$migrator->run();