apiClient = new ApiClient($apiEndpoint); $this->routerData = []; } /** * Set progress callback function */ public function setProgressCallback($callback) { $this->progressCallback = $callback; } /** * Report progress */ private function reportProgress($step, $message, $percentage = null) { logMessage('INFO', "[$step] $message"); if ($this->progressCallback && is_callable($this->progressCallback)) { call_user_func($this->progressCallback, $step, $message, $percentage); } } /** * Main enrollment process */ public function enrollRouter($hashKey, $apiEndpoint = null) { try { if ($apiEndpoint) { $this->apiClient = new ApiClient($apiEndpoint); } $this->reportProgress('INIT', 'Starting router enrollment process...', 0); // Step 1: Check system requirements $this->reportProgress('REQUIREMENTS', 'Checking system requirements...', 10); if (!$this->checkSystemRequirements()) { throw new Exception('System requirements check failed'); } // Step 2: Install OpenZiti if needed $this->reportProgress('INSTALL', 'Installing OpenZiti CLI...', 20); if (!$this->installZiti()) { throw new Exception('OpenZiti installation failed'); } // Step 3: Create directories $this->reportProgress('DIRECTORIES', 'Creating necessary directories...', 30); if (!$this->createDirectories()) { throw new Exception('Failed to create directories'); } // Step 4: Register router with API $this->reportProgress('REGISTER', 'Registering router with ZitiNexus Portal...', 40); $result = $this->apiClient->registerRouter($hashKey); if (!$result['success']) { throw new Exception('Router registration failed: ' . $result['error']); } $this->routerData = $result['data']; $this->reportProgress('REGISTER', 'Router registered successfully: ' . $this->routerData['routerInfo']['name'], 50); // Step 5: Save configuration files $this->reportProgress('CONFIG', 'Saving configuration files...', 60); if (!$this->saveConfiguration()) { throw new Exception('Failed to save configuration files'); } // Step 6: Enroll router with OpenZiti $this->reportProgress('ENROLL', 'Enrolling router with OpenZiti controller...', 70); if (!$this->enrollWithZiti()) { throw new Exception('Router enrollment with OpenZiti failed'); } // Step 7: Create systemd service $this->reportProgress('SERVICE', 'Creating systemd service...', 80); if (!$this->createSystemdService()) { throw new Exception('Failed to create systemd service'); } // Step 8: Start router service $this->reportProgress('START', 'Starting router service...', 90); if (!$this->startRouter()) { throw new Exception('Failed to start router service'); } // Step 9: Report success status $this->reportProgress('REPORT', 'Reporting enrollment status...', 95); $this->reportSuccessStatus($hashKey); $this->reportProgress('COMPLETE', 'Router enrollment completed successfully!', 100); return [ 'success' => true, 'routerName' => $this->routerData['routerInfo']['name'], 'routerId' => $this->routerData['routerInfo']['id'], 'message' => 'Router enrollment completed successfully' ]; } catch (Exception $e) { $errorMsg = $e->getMessage(); logMessage('ERROR', $errorMsg); $this->reportProgress('ERROR', $errorMsg, null); // Report failure status if (!empty($hashKey) && !empty($this->routerData['callbackUrl'])) { $this->apiClient->reportStatus( $this->routerData['callbackUrl'], $hashKey, 'failed', null, $errorMsg ); } return [ 'success' => false, 'error' => $errorMsg ]; } } /** * Check system requirements */ private function checkSystemRequirements() { // Check if running as root if (!isRunningAsRoot()) { throw new Exception('This script must be run as root (use sudo)'); } // Check if curl is available if (!$this->checkCommand('curl')) { $this->reportProgress('REQUIREMENTS', 'Installing curl...'); if (!$this->installPackage('curl')) { return false; } } // Check if jq is available if (!$this->checkCommand('jq')) { $this->reportProgress('REQUIREMENTS', 'Installing jq...'); if (!$this->installPackage('jq')) { return false; } } // Check if systemctl is available if (!$this->checkCommand('systemctl')) { throw new Exception('systemctl is required but not available'); } return true; } /** * Install OpenZiti CLI */ private function installZiti() { // Check if ziti is already installed if ($this->checkCommand('ziti')) { $output = ''; executeCommand('ziti version 2>/dev/null | head -n1', $output); $this->reportProgress('INSTALL', 'OpenZiti CLI already installed: ' . trim($output)); return true; } $this->reportProgress('INSTALL', 'Setting up OpenZiti package repository...'); // Add GPG key using enhanced method to handle PHP execution environment if (!$this->addOpenZitiGpgKey()) { throw new Exception('Failed to add OpenZiti GPG key'); } // Set proper permissions if (!executeCommand('chmod a+r /usr/share/keyrings/openziti.gpg')) { throw new Exception('Failed to set GPG key permissions'); } // Add repository to sources list $repoContent = 'deb [signed-by=/usr/share/keyrings/openziti.gpg] https://packages.openziti.org/zitipax-openziti-deb-stable debian main'; $tempFile = tempnam(sys_get_temp_dir(), 'openziti-repo'); file_put_contents($tempFile, $repoContent); if (!executeCommand("cp '$tempFile' /etc/apt/sources.list.d/openziti-release.list")) { unlink($tempFile); throw new Exception('Failed to add OpenZiti repository'); } unlink($tempFile); // Update package list $this->reportProgress('INSTALL', 'Updating package list...'); if (!executeCommand('apt-get update')) { throw new Exception('Failed to update package list'); } // Install openziti-router package $this->reportProgress('INSTALL', 'Installing openziti-router package...'); if (!executeCommand('apt-get install -y openziti-router')) { $this->reportProgress('INSTALL', 'Trying to install ziti CLI only...'); if (!executeCommand('apt-get install -y ziti')) { throw new Exception('Failed to install OpenZiti CLI'); } } // Verify installation if (!$this->checkCommand('ziti')) { throw new Exception('OpenZiti CLI installation failed - command not found after installation'); } $output = ''; executeCommand('ziti version 2>/dev/null | head -n1', $output); $this->reportProgress('INSTALL', 'OpenZiti CLI installed successfully: ' . trim($output)); return true; } /** * Create necessary directories */ private function createDirectories() { $directories = [ CONFIG_DIR => 0755, CERTS_DIR => 0700, dirname(LOG_FILE) => 0755 ]; foreach ($directories as $dir => $permissions) { if (!is_dir($dir)) { // Use sudo to create system directories if (!executeCommand("mkdir -p '$dir'")) { throw new Exception("Failed to create directory: $dir"); } if (!executeCommand("chmod " . decoct($permissions) . " '$dir'")) { throw new Exception("Failed to set permissions for directory: $dir"); } } else { // Ensure permissions are correct even if directory exists executeCommand("chmod " . decoct($permissions) . " '$dir'"); } } return true; } /** * Save configuration files */ private function saveConfiguration() { // Save JWT using temp file and sudo $tempJwtFile = tempnam(sys_get_temp_dir(), 'ziti-jwt'); file_put_contents($tempJwtFile, $this->routerData['jwt']); if (!executeCommand("cp '$tempJwtFile' " . JWT_FILE)) { unlink($tempJwtFile); throw new Exception('Failed to save JWT file'); } unlink($tempJwtFile); if (!executeCommand("chmod 600 " . JWT_FILE)) { throw new Exception('Failed to set JWT file permissions'); } // Save router configuration using temp file and sudo $tempConfigFile = tempnam(sys_get_temp_dir(), 'ziti-config'); file_put_contents($tempConfigFile, $this->routerData['routerConfig']['yaml']); if (!executeCommand("cp '$tempConfigFile' " . ROUTER_CONFIG)) { unlink($tempConfigFile); throw new Exception('Failed to save router configuration'); } unlink($tempConfigFile); if (!executeCommand("chmod 644 " . ROUTER_CONFIG)) { throw new Exception('Failed to set router config permissions'); } // Fix router configuration for proper enrollment $this->fixRouterConfiguration(); return true; } /** * Fix router configuration (replicate bash script logic) */ private function fixRouterConfiguration() { // Create backup using sudo executeCommand("cp " . ROUTER_CONFIG . " " . ROUTER_CONFIG . ".backup"); $routerName = $this->routerData['routerInfo']['name']; $routerId = $this->routerData['routerInfo']['id']; $tenantId = $this->routerData['metadata']['tenantId']; $controllerEndpoint = $this->routerData['metadata']['controllerEndpoint'] ?? 'enroll.zitinexus.com:443'; // Add tls: prefix if not present if (strpos($controllerEndpoint, 'tls:') !== 0) { $controllerEndpoint = 'tls:' . $controllerEndpoint; } // Build role attributes $roleAttributesSection = '# No role attributes specified'; if (!empty($this->routerData['routerInfo']['roleAttributes'])) { $roleAttributesSection = "roleAttributes:"; foreach ($this->routerData['routerInfo']['roleAttributes'] as $attr) { $roleAttributesSection .= "\n - \"$attr\""; } } $generatedAt = date('c'); $configContent = <<&1'; $output = ''; if (!executeCommand($command, $output)) { throw new Exception('Router enrollment failed: ' . $output); } // Verify certificates were created using sudo (since certs are root-owned with 600 permissions) $routerName = $this->routerData['routerInfo']['name']; $certFile = CERTS_DIR . '/' . $routerName . '.cert'; // Use sudo to check if certificate file exists (www-data can't read root-owned 600 files) $checkOutput = ''; if (!executeCommand("test -f '$certFile'", $checkOutput)) { // List what files actually exist for debugging $listOutput = ''; executeCommand("ls -la " . CERTS_DIR . "/", $listOutput); throw new Exception("Router certificate not found after enrollment. Expected: $certFile. Files in certs directory: " . $listOutput); } return true; } /** * Create systemd service */ private function createSystemdService() { $finalConfig = '/etc/zitirouter/zitirouter.yaml'; // Copy router config to final location using sudo if (!executeCommand("cp " . ROUTER_CONFIG . " '$finalConfig'")) { throw new Exception('Failed to copy router config to final location'); } if (!executeCommand("chmod 644 '$finalConfig'")) { throw new Exception('Failed to set final config permissions'); } $serviceContent = <<routerData['callbackUrl'])) { return; } $hostname = ''; $arch = ''; $os = ''; $zitiVersion = ''; executeCommand('hostname', $hostname); executeCommand('uname -m', $arch); executeCommand('lsb_release -d 2>/dev/null | cut -f2', $os); executeCommand('ziti version 2>/dev/null | head -n1', $zitiVersion); $routerInfo = [ 'version' => trim($zitiVersion) ?: 'unknown', 'hostname' => trim($hostname), 'arch' => trim($arch), 'os' => trim($os) ?: 'Linux' ]; $this->apiClient->reportStatus( $this->routerData['callbackUrl'], $hashKey, 'success', $routerInfo ); } /** * Add OpenZiti GPG key with enhanced error handling for PHP execution environment */ private function addOpenZitiGpgKey() { $gpgKeyUrl = 'https://get.openziti.io/tun/package-repos.gpg'; $gpgKeyPath = '/usr/share/keyrings/openziti.gpg'; // First, check if GPG key already exists and is valid if (file_exists($gpgKeyPath) && filesize($gpgKeyPath) > 0) { $this->reportProgress('INSTALL', 'OpenZiti GPG key already exists (' . filesize($gpgKeyPath) . ' bytes), skipping installation'); logMessage('INFO', 'GPG key already exists at: ' . $gpgKeyPath); return true; } $this->reportProgress('INSTALL', 'Downloading OpenZiti GPG key...'); // Method 1: Try the original piped command with enhanced environment // First, try the enhanced piped command with explicit environment $envCommand = 'export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" && export HOME="/root" && export GNUPGHOME="/root/.gnupg"'; $gpgCommand = $envCommand . ' && curl -sSLf ' . $gpgKeyUrl . ' | gpg --dearmor --output ' . $gpgKeyPath; $output = ''; if (executeCommand($gpgCommand, $output)) { $this->reportProgress('INSTALL', 'GPG key added successfully using piped method'); return true; } logMessage('WARNING', 'Piped GPG command failed: ' . $output); $this->reportProgress('INSTALL', 'Piped method failed, trying step-by-step approach...'); // Method 2: Step-by-step approach - download first, then process $tempGpgFile = tempnam(sys_get_temp_dir(), 'openziti-gpg'); // Step 1: Download GPG key to temporary file $downloadCommand = 'curl -sSLf ' . $gpgKeyUrl . ' -o ' . $tempGpgFile; if (!executeCommand($downloadCommand, $output)) { unlink($tempGpgFile); logMessage('ERROR', 'Failed to download GPG key: ' . $output); return false; } // Step 2: Verify the downloaded file exists and has content if (!file_exists($tempGpgFile) || filesize($tempGpgFile) == 0) { unlink($tempGpgFile); logMessage('ERROR', 'Downloaded GPG key file is empty or missing'); return false; } $this->reportProgress('INSTALL', 'GPG key downloaded successfully, processing...'); // Step 3: Process with GPG using explicit environment and full paths $gpgProcessCommand = $envCommand . ' && /usr/bin/gpg --dearmor --output ' . $gpgKeyPath . ' ' . $tempGpgFile; if (executeCommand($gpgProcessCommand, $output)) { unlink($tempGpgFile); $this->reportProgress('INSTALL', 'GPG key processed successfully using step-by-step method'); return true; } logMessage('ERROR', 'GPG processing failed: ' . $output); // Method 3: Fallback - use cat and redirect (sometimes works when pipes don't) $this->reportProgress('INSTALL', 'Trying fallback method...'); $fallbackCommand = $envCommand . ' && cat ' . $tempGpgFile . ' | /usr/bin/gpg --dearmor > ' . $gpgKeyPath; if (executeCommand($fallbackCommand, $output)) { unlink($tempGpgFile); $this->reportProgress('INSTALL', 'GPG key added successfully using fallback method'); return true; } // Clean up and log final failure unlink($tempGpgFile); logMessage('ERROR', 'All GPG key installation methods failed. Last error: ' . $output); // Method 4: Last resort - manual file operations $this->reportProgress('INSTALL', 'Trying manual file operations as last resort...'); // Download again to a new temp file $tempGpgFile2 = tempnam(sys_get_temp_dir(), 'openziti-gpg2'); if (executeCommand('curl -sSLf ' . $gpgKeyUrl . ' -o ' . $tempGpgFile2, $output)) { // Try to use openssl or other tools if available $opensslCommand = 'openssl base64 -d -A < ' . $tempGpgFile2 . ' > ' . $gpgKeyPath; if (executeCommand($opensslCommand, $output)) { unlink($tempGpgFile2); $this->reportProgress('INSTALL', 'GPG key added using openssl fallback'); return true; } // Final attempt: just copy the raw file and let apt handle it if (executeCommand("cp '$tempGpgFile2' '$gpgKeyPath'", $output)) { unlink($tempGpgFile2); $this->reportProgress('INSTALL', 'GPG key copied as raw file - apt may handle conversion'); return true; } unlink($tempGpgFile2); } return false; } /** * Check if command exists */ private function checkCommand($command) { $output = ''; return executeCommand("which $command", $output); } /** * Install package using apt */ private function installPackage($package) { return executeCommand("apt-get update && apt-get install -y $package"); } /** * Get system status information */ public function getSystemStatus() { $status = [ 'hostname' => '', 'ziti_status' => 'unknown', 'ziti_version' => '', 'service_active' => false, 'config_exists' => false, 'certificates_exist' => false ]; // Get hostname executeCommand('hostname', $status['hostname']); $status['hostname'] = trim($status['hostname']); // Check if ziti command exists and get version if ($this->checkCommand('ziti')) { executeCommand('ziti version 2>/dev/null | head -n1', $status['ziti_version']); $status['ziti_version'] = trim($status['ziti_version']); $status['ziti_status'] = 'installed'; } else { $status['ziti_status'] = 'not_installed'; } // Check service status $output = ''; if (executeCommand('systemctl is-active ziti-router.service 2>/dev/null', $output)) { $status['service_active'] = trim($output) === 'active'; } // Check configuration files $status['config_exists'] = file_exists(ROUTER_CONFIG); // Check certificates if (is_dir(CERTS_DIR)) { $certFiles = glob(CERTS_DIR . '/*.cert'); $status['certificates_exist'] = !empty($certFiles); } return $status; } } ?>