* @link https://arkhost.com */ if (!defined('WHMCS')) { http_response_code(403); exit('Access denied'); } use WHMCS\Config\Setting; use WHMCS\Database\Capsule; function ArkHostHetznerVPS_GetVPSID(array $params) { // Check if we have model (new way) - ArkHostHetznerVPS format if (isset($params['model']) && is_object($params['model']) && isset($params['model']->serviceProperties)) { // Try our new format first $vpsId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|VPS ID'); if ($vpsId) { return str_replace(['ArkHostHetznerVPS-', 'VPS-'], '', $vpsId); } // Try ModulesGarden format for migration compatibility $serverId = $params['model']->serviceProperties->get('serverID|Server ID'); if ($serverId) { return str_replace(['server-', 'Server-'], '', $serverId); } } // Check if we have customfields (alternative way) if (isset($params['customfields']) && is_array($params['customfields'])) { foreach ($params['customfields'] as $field => $value) { // Check for our new format: ArkHostHetznerVPS|VPS ID if (stripos($field, 'ArkHostHetznerVPS') !== false && stripos($field, 'VPS ID') !== false) { return str_replace(['ArkHostHetznerVPS-', 'VPS-'], '', $value); } // Check for ModulesGarden format: serverID|Server ID (MIGRATION COMPATIBILITY) if (stripos($field, 'serverID') !== false && stripos($field, 'Server ID') !== false) { return str_replace(['server-', 'Server-'], '', $value); } // Fallback: any field containing VPS ID if (stripos($field, 'VPS ID') !== false && !empty($value)) { return str_replace(['ArkHostHetznerVPS-', 'VPS-', 'server-', 'Server-'], '', $value); } } } // For ModulesGarden migration - check if server ID is in domain field if (isset($params['domain']) && is_numeric($params['domain'])) { return $params['domain']; } // For ModulesGarden migration - check username field if (isset($params['username']) && is_numeric($params['username'])) { return $params['username']; } // Check customfields for any numeric server ID (broader compatibility) if (isset($params['customfields']) && is_array($params['customfields'])) { foreach ($params['customfields'] as $field => $value) { // ModulesGarden might store it as "Server ID" or similar if ((stripos($field, 'server') !== false || stripos($field, 'vps') !== false) && is_numeric($value)) { return $value; } } } // Check if there's a dedicatedip field with server ID (some modules use this) if (isset($params['dedicatedip']) && preg_match('/(\d{6,})/', $params['dedicatedip'], $matches)) { return $matches[1]; } return null; } function ArkHostHetznerVPS_GetConfigurableOption(array $params, $optionName) { // Check for WHMCS Configurable Options (add-ons) that customer has ordered if (isset($params['configoptions']) && is_array($params['configoptions'])) { foreach ($params['configoptions'] as $optName => $optValue) { // Check if option name contains the floating IP identifier if (stripos($optName, $optionName) !== false && !empty($optValue)) { return $optValue; } } } // Also check by option ID if available if (isset($params['configoptionshash']) && is_array($params['configoptionshash'])) { foreach ($params['configoptionshash'] as $optId => $optValue) { if (stripos($optId, $optionName) !== false && !empty($optValue)) { return $optValue; } } } return null; } function ArkHostHetznerVPS_API(array $params) { $url = 'https://api.hetzner.cloud/v1/'; $data = []; $method = ''; switch ($params['action']) { case 'Test': $url .= 'server_types?per_page=1'; $method = 'GET'; break; case 'Packages': $url .= 'server_types?per_page=50'; $method = 'GET'; break; case 'Operating Systems': $url .= 'images?type=system&per_page=50'; $method = 'GET'; break; case 'Datacenters': $url .= 'datacenters'; $method = 'GET'; break; case 'Upgrades': // Not supported in Hetzner API return array(); break; case 'Discount': // Hetzner doesn't have a discount endpoint, return dummy data return array('percent' => 0); break; case 'Balance': // Hetzner doesn't have a balance endpoint, return dummy data return array('balance' => 'N/A'); break; case 'Order': $url .= 'servers'; $method = 'POST'; $data = array( 'name' => $params['domain'] ?? 'vps-' . time(), 'server_type' => ArkHostHetznerVPS_GetOption($params, 'planid'), 'image' => ArkHostHetznerVPS_GetOption($params, 'osid'), 'start_after_create' => true, 'labels' => array( 'whmcs_service_id' => (string)$params['serviceid'], 'whmcs_user_id' => (string)$params['userid'] ) ); // Add datacenter if specified $datacenter = ArkHostHetznerVPS_GetOption($params, 'datacenter'); if ($datacenter) { $data['datacenter'] = $datacenter; } // Handle backups if (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on') { $data['automount'] = false; $data['backups'] = true; } // Handle IPv6 if (ArkHostHetznerVPS_GetOption($params, 'ipv6') === 'on') { $data['enable_ipv6'] = false; } // Handle Cloud-Init user_data if provided $cloudInitYaml = ArkHostHetznerVPS_GetOption($params, 'cloud_init_yaml'); if ($cloudInitYaml && trim($cloudInitYaml) !== '') { // Basic YAML validation - check if it starts with #cloud-config $trimmedYaml = trim($cloudInitYaml); if (strpos($trimmedYaml, '#cloud-config') !== 0) { // Auto-prepend #cloud-config if missing $cloudInitYaml = "#cloud-config\n" . $cloudInitYaml; } // Pass as plain text - Hetzner API accepts user_data as plain string (max 32KiB) $data['user_data'] = $cloudInitYaml; } // Handle SSH keys from custom field if needed // This would need to be implemented with a custom field break; case 'Server Info': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); $method = 'GET'; break; case 'Label': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); $method = 'PUT'; $data = array( 'name' => $params['label'], ); break; case 'Graphs': // Hetzner metrics endpoint requires specific format $serverId = ArkHostHetznerVPS_GetVPSID($params); // Determine time range based on period $period = $params['time'] ?? 'day'; $end = time(); switch ($period) { case 'hour': $start = $end - 3600; // 1 hour $step = 60; // 1 minute intervals break; case 'day': $start = $end - 86400; // 24 hours $step = 300; // 5 minute intervals break; case 'week': $start = $end - 604800; // 7 days $step = 1800; // 30 minute intervals break; case 'month': $start = $end - 2592000; // 30 days $step = 7200; // 2 hour intervals break; case 'year': $start = $end - 31536000; // 365 days $step = 86400; // 1 day intervals break; default: $start = $end - 86400; $step = 300; } // Get all metric types at once $metricType = 'cpu,disk,network'; // Format timestamps in ISO-8601 - Hetzner requires specific format without timezone $startISO = gmdate('Y-m-d\TH:i:s\Z', $start); $endISO = gmdate('Y-m-d\TH:i:s\Z', $end); $url .= 'servers/' . $serverId . '/metrics?type=' . $metricType . '&start=' . $startISO . '&end=' . $endISO . '&step=' . $step; $method = 'GET'; break; case 'Operating Systems - Server': $url .= 'images?type=system&per_page=50'; $method = 'GET'; break; case 'Cancel': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); $method = 'DELETE'; break; case 'Stop Cancellation': // Not supported in Hetzner API return array('success' => true); break; case 'VNC Console': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/request_console'; $method = 'POST'; // No data needed for request_console break; case 'Reinstall': // Rebuild server with new image (destroys all data) $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/rebuild'; $method = 'POST'; $data = array( 'image' => $params['os'] // Can be image ID or name (e.g. "ubuntu-20.04") ); break; case 'Reboot': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/reboot'; $method = 'POST'; break; case 'Stop': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/poweroff'; $method = 'POST'; break; case 'Shutdown': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/shutdown'; $method = 'POST'; break; case 'Start': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/poweron'; $method = 'POST'; break; case 'Disable': // Use poweroff for disable in Hetzner $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/poweroff'; $method = 'POST'; break; case 'Enable': // Use poweron for enable in Hetzner $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/poweron'; $method = 'POST'; break; case 'IPv4 Addresses': // Get server info which includes IP addresses $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); $method = 'GET'; break; case 'Reverse DNS': // Hetzner handles rDNS differently return array('success' => false, 'message' => 'Not implemented'); break; case 'Addons': // Not supported in Hetzner API return array(); break; case 'Upgrade': // Hetzner requires creating a new server return array('success' => false, 'message' => 'Server upgrades not supported'); break; case 'Hostname rDNS': // Update server name $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); $method = 'PUT'; $data = array( 'name' => $params['hostname'] ); break; case 'Create backup': // Create manual backup (still type=backup, but with distinct description) $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image'; $method = 'POST'; $data = array( 'description' => 'Manual backup - ' . date('Y-m-d H:i:s'), 'type' => 'backup' ); break; case 'Delete backup': // Delete image $imageId = $params['image_id'] ?? $params['file'] ?? ''; $url .= 'images/' . $imageId; $method = 'DELETE'; break; case 'List backups': // List images of type backup $url .= 'images?type=backup&bound_to=' . ArkHostHetznerVPS_GetVPSID($params); $method = 'GET'; break; case 'Restore backup': // Rebuild from image $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/rebuild'; $method = 'POST'; $data = array( 'image' => $params['image_id'] ?? $params['file'] ?? '' ); break; case 'Get Firewall rules': // Get server info first to find attached firewalls $serverId = ArkHostHetznerVPS_GetVPSID($params); $url .= 'servers/' . $serverId; $method = 'GET'; break; case 'List Firewalls': // List all available firewalls $url .= 'firewalls'; $method = 'GET'; break; case 'Create Firewall': $url .= 'firewalls'; $method = 'POST'; $data = array( 'name' => $params['name'] ?? 'firewall-' . time(), 'rules' => isset($params['rules']) ? $params['rules'] : array() ); break; case 'Apply Firewall': $url .= 'firewalls/' . $params['firewall_id'] . '/actions/apply_to_resources'; $method = 'POST'; $serverId = ArkHostHetznerVPS_GetVPSID($params); $data = array( 'apply_to' => array( array( 'type' => 'server', 'server' => array( 'id' => intval($serverId) ) ) ) ); break; case 'Get Firewall Details': $url .= 'firewalls/' . $params['firewall_id']; $method = 'GET'; break; case 'Update Firewall Rules': $url .= 'firewalls/' . $params['firewall_id'] . '/actions/set_rules'; $method = 'POST'; $data = array( 'rules' => $params['rules'] ); break; case 'Remove Firewall': $url .= 'firewalls/' . $params['firewall_id'] . '/actions/remove_from_resources'; $method = 'POST'; $data = array( 'remove_from' => array( array( 'type' => 'server', 'server' => ArkHostHetznerVPS_GetVPSID($params) ) ) ); break; case 'Add Firewall rules': // For Hetzner, we need to check if a firewall exists, create one if not, then update rules $serverId = ArkHostHetznerVPS_GetVPSID($params); // First, get server info to check if firewall is attached $serverParams = $params; $serverParams['action'] = 'Server Info'; $serverInfo = ArkHostHetznerVPS_API($serverParams); $firewallId = null; if (isset($serverInfo['server']['public_net']['firewalls']) && !empty($serverInfo['server']['public_net']['firewalls'])) { // Use existing firewall $firewallId = $serverInfo['server']['public_net']['firewalls'][0]['id']; // Get current firewall rules $firewallParams = $params; $firewallParams['action'] = 'Get Firewall Details'; $firewallParams['firewall_id'] = $firewallId; $firewallDetails = ArkHostHetznerVPS_API($firewallParams); $existingRules = isset($firewallDetails['firewall']['rules']) ? $firewallDetails['firewall']['rules'] : array(); } else { // No firewall attached, create one $createParams = $params; $createParams['action'] = 'Create Firewall'; // Simple unique name $createParams['name'] = 'server-' . $serverId . '-' . time(); $createParams['rules'] = array(); try { $createResult = ArkHostHetznerVPS_API($createParams); } catch (Exception $e) { // If name is still not unique, try with random suffix if (strpos($e->getMessage(), 'uniqueness_error') !== false || strpos($e->getMessage(), 'name is already used') !== false) { $createParams['name'] = 'firewall-server-' . $serverId . '-' . time() . '-' . rand(1000, 9999); $createResult = ArkHostHetznerVPS_API($createParams); } else { throw $e; } } $firewallId = $createResult['firewall']['id']; // Attach firewall to server $attachParams = $params; $attachParams['action'] = 'Apply Firewall'; $attachParams['firewall_id'] = $firewallId; $attachResult = ArkHostHetznerVPS_API($attachParams); // Wait a moment for the firewall to be applied sleep(2); // Verify the firewall was attached $verifyParams = $params; $verifyParams['action'] = 'Server Info'; $verifyInfo = ArkHostHetznerVPS_API($verifyParams); // Check if firewall is actually attached if (!isset($verifyInfo['server']['public_net']['firewalls']) || empty($verifyInfo['server']['public_net']['firewalls']) || $verifyInfo['server']['public_net']['firewalls'][0]['id'] != $firewallId) { // Try to attach again if it failed $attachParams = $params; $attachParams['action'] = 'Apply Firewall'; $attachParams['firewall_id'] = $firewallId; ArkHostHetznerVPS_API($attachParams); } $existingRules = array(); } // Add new rule to existing rules // Handle protocol - Hetzner API doesn't accept "ANY", we need to create multiple rules $protocol = strtolower($params['protocol']); $rulesToAdd = array(); if ($protocol === 'any') { // Create rules for tcp and udp when "ANY" is selected $protocols = array('tcp', 'udp'); foreach ($protocols as $proto) { $rule = array( 'direction' => isset($params['direction']) ? $params['direction'] : 'in', 'protocol' => $proto, ); // Handle IPs - use source_ips for inbound, destination_ips for outbound $direction = isset($params['direction']) ? $params['direction'] : 'in'; if (!empty($params['source']) && $params['source'] !== 'Any') { $sourceIp = trim($params['source']); // If no CIDR notation, add /32 for IPv4 or /128 for IPv6 if (strpos($sourceIp, '/') === false) { if (filter_var($sourceIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $sourceIp .= '/128'; } else { $sourceIp .= '/32'; } } if ($direction === 'out') { $rule['destination_ips'] = array($sourceIp); $rule['source_ips'] = array(); } else { $rule['source_ips'] = array($sourceIp); $rule['destination_ips'] = array(); } } else { // For "Any" or empty, use 0.0.0.0/0 and ::/0 for all IPs if ($direction === 'out') { $rule['destination_ips'] = array('0.0.0.0/0', '::/0'); $rule['source_ips'] = array(); } else { $rule['source_ips'] = array('0.0.0.0/0', '::/0'); $rule['destination_ips'] = array(); } } // Handle port - Hetzner expects port as a string if (!empty($params['port'])) { $rule['port'] = strval($params['port']); } $rulesToAdd[] = $rule; } } else { // Single protocol rule $newRule = array( 'direction' => isset($params['direction']) ? $params['direction'] : 'in', 'protocol' => $protocol, ); // Handle IPs - use source_ips for inbound, destination_ips for outbound $direction = isset($params['direction']) ? $params['direction'] : 'in'; if (!empty($params['source']) && $params['source'] !== 'Any') { $sourceIp = trim($params['source']); // If no CIDR notation, add /32 for IPv4 or /128 for IPv6 if (strpos($sourceIp, '/') === false) { if (filter_var($sourceIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $sourceIp .= '/128'; } else { $sourceIp .= '/32'; } } if ($direction === 'out') { $newRule['destination_ips'] = array($sourceIp); $newRule['source_ips'] = array(); } else { $newRule['source_ips'] = array($sourceIp); $newRule['destination_ips'] = array(); } } else { // For "Any" or empty, use 0.0.0.0/0 and ::/0 for all IPs if ($direction === 'out') { $newRule['destination_ips'] = array('0.0.0.0/0', '::/0'); $newRule['source_ips'] = array(); } else { $newRule['source_ips'] = array('0.0.0.0/0', '::/0'); $newRule['destination_ips'] = array(); } } // Handle port - Hetzner expects port as a string if (!empty($params['port']) && $protocol !== 'icmp') { $newRule['port'] = strval($params['port']); } $rulesToAdd[] = $newRule; } // Add the new rules to existing rules // Note: Hetzner only supports ACCEPT rules for inbound traffic // The default policy is DROP for non-matched traffic foreach ($rulesToAdd as $ruleToAdd) { $existingRules[] = $ruleToAdd; } // Update firewall with new rules $updateParams = $params; $updateParams['action'] = 'Update Firewall Rules'; $updateParams['firewall_id'] = $firewallId; $updateParams['rules'] = $existingRules; ArkHostHetznerVPS_API($updateParams); return array('success' => true, 'message' => 'Firewall rule added successfully'); break; case 'Delete Firewall rule': // For Hetzner, we need to get all rules, remove the one, and update $serverId = ArkHostHetznerVPS_GetVPSID($params); // Get server info to find attached firewall $serverParams = $params; $serverParams['action'] = 'Server Info'; $serverInfo = ArkHostHetznerVPS_API($serverParams); if (!isset($serverInfo['server']['public_net']['firewalls']) || empty($serverInfo['server']['public_net']['firewalls'])) { return array('success' => false, 'message' => 'No firewall attached to this server'); } $firewallId = $serverInfo['server']['public_net']['firewalls'][0]['id']; // Get current firewall rules $firewallParams = $params; $firewallParams['action'] = 'Get Firewall Details'; $firewallParams['firewall_id'] = $firewallId; $firewallDetails = ArkHostHetznerVPS_API($firewallParams); $existingRules = isset($firewallDetails['firewall']['rules']) ? $firewallDetails['firewall']['rules'] : array(); $newRules = array(); // Remove the rule with matching ID (we use index as ID) $ruleIdToDelete = $params['rule_id']; $inIndex = 0; $outIndex = 0; foreach ($existingRules as $rule) { if ($rule['direction'] === 'in') { if ('fw_' . $firewallId . '_in_' . $inIndex != $ruleIdToDelete) { $newRules[] = $rule; } $inIndex++; } else if ($rule['direction'] === 'out') { if ('fw_' . $firewallId . '_out_' . $outIndex != $ruleIdToDelete) { $newRules[] = $rule; } $outIndex++; } else { // Keep other rules $newRules[] = $rule; } } // Update firewall with remaining rules $updateParams = $params; $updateParams['action'] = 'Update Firewall Rules'; $updateParams['firewall_id'] = $firewallId; $updateParams['rules'] = $newRules; ArkHostHetznerVPS_API($updateParams); return array('success' => true, 'message' => 'Firewall rule deleted successfully'); break; case 'Commit Firewall rules': // Not applicable for Hetzner - changes are immediate return array('success' => true, 'message' => 'Firewall changes are applied immediately in Hetzner'); break; case 'ISO Images': $url .= 'isos'; $method = 'GET'; break; case 'Load ISO': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/attach_iso'; $method = 'POST'; // Hetzner expects the ISO name, not ID $data = array('iso' => $params['iso_id']); break; case 'Eject ISO': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/detach_iso'; $method = 'POST'; break; case 'Reset root': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/reset_password'; $method = 'POST'; break; case 'Server Metrics': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/metrics'; $method = 'GET'; break; case 'Create Snapshot': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image'; $method = 'POST'; $data = array( 'description' => $params['description'] ?? 'Snapshot created on ' . date('Y-m-d H:i:s'), 'type' => 'snapshot' ); break; case 'List Snapshots': $url .= 'images?type=snapshot&bound_to=' . ArkHostHetznerVPS_GetVPSID($params); $method = 'GET'; break; case 'Floating IPs': $url .= 'floating_ips'; $method = 'GET'; break; case 'Get Floating IP': $url .= 'floating_ips/' . $params['floating_ip_id']; $method = 'GET'; break; case 'Assign Floating IP': $url .= 'floating_ips/' . $params['floating_ip_id'] . '/actions/assign'; $method = 'POST'; $data = array( 'server' => ArkHostHetznerVPS_GetVPSID($params) ); break; case 'Unassign Floating IP': $url .= 'floating_ips/' . $params['floating_ip_id'] . '/actions/unassign'; $method = 'POST'; break; case 'Create Floating IP': $url .= 'floating_ips'; $method = 'POST'; $data = array( 'type' => $params['ip_type'] ?? 'ipv4', 'description' => $params['description'] ?? 'Created via WHMCS', 'labels' => $params['labels'] ?? array(), 'home_location' => $params['location'] ?? null, 'server' => isset($params['assign_to_server']) ? ArkHostHetznerVPS_GetVPSID($params) : null ); break; case 'Delete Floating IP': $url .= 'floating_ips/' . $params['floating_ip_id']; $method = 'DELETE'; break; case 'Update Floating IP': $url .= 'floating_ips/' . $params['floating_ip_id']; $method = 'PUT'; $data = array( 'description' => $params['description'] ?? null, 'labels' => $params['labels'] ?? null ); break; case 'Change Floating IP DNS': $url .= 'floating_ips/' . $params['floating_ip_id'] . '/actions/change_dns_ptr'; $method = 'POST'; $data = array( 'ip' => $params['ip'], 'dns_ptr' => $params['dns_ptr'] ); break; case 'Server Actions': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions'; $method = 'GET'; break; case 'Change Protection': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/change_protection'; $method = 'POST'; $data = array( 'delete' => isset($params['delete_protection']) ? $params['delete_protection'] : false, 'rebuild' => isset($params['rebuild_protection']) ? $params['rebuild_protection'] : false ); break; case 'Rescue Mode': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/enable_rescue'; $method = 'POST'; $data = array( 'type' => $params['rescue_type'] ?? 'linux64', ); break; case 'Disable Rescue Mode': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/disable_rescue'; $method = 'POST'; break; case 'Enable Backups': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/enable_backup'; $method = 'POST'; break; case 'Disable Backups': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/disable_backup'; $method = 'POST'; break; case 'List Volumes': $url .= 'volumes?server=' . ArkHostHetznerVPS_GetVPSID($params); $method = 'GET'; break; case 'Create Volume': $url .= 'volumes'; $method = 'POST'; $data = array( 'size' => $params['size'] ?? 10, 'name' => $params['name'] ?? 'volume-' . time(), 'server' => ArkHostHetznerVPS_GetVPSID($params), 'automount' => isset($params['automount']) ? $params['automount'] : true, 'format' => $params['format'] ?? 'ext4' ); break; case 'Attach Volume': $url .= 'volumes/' . $params['volume_id'] . '/actions/attach'; $method = 'POST'; $data = array( 'server' => ArkHostHetznerVPS_GetVPSID($params), 'automount' => isset($params['automount']) ? $params['automount'] : true ); break; case 'Detach Volume': $url .= 'volumes/' . $params['volume_id'] . '/actions/detach'; $method = 'POST'; break; case 'Delete Volume': $url .= 'volumes/' . $params['volume_id']; $method = 'DELETE'; break; case 'List Networks': $url .= 'networks'; $method = 'GET'; break; case 'Attach to Network': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/attach_to_network'; $method = 'POST'; $data = array( 'network' => $params['network_id'], 'ip' => isset($params['ip']) ? $params['ip'] : null, 'alias_ips' => isset($params['alias_ips']) ? $params['alias_ips'] : array() ); break; case 'Detach from Network': $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/detach_from_network'; $method = 'POST'; $data = array( 'network' => $params['network_id'] ); break; default: throw new Exception('Invalid action: ' . $params['action']); break; } $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($curl, CURLOPT_TIMEOUT, 15); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POSTREDIR, CURL_REDIR_POST_301); curl_setopt($curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); curl_setopt($curl, CURLOPT_USERAGENT, 'ArkHostHetznerVPS WHMCS'); curl_setopt($curl, CURLOPT_HTTPHEADER, array( 'Authorization: Bearer ' . $params['serverpassword'], 'Content-Type: application/json' )); if ($method === 'POST' || $method === 'PATCH' || $method === 'PUT') { curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); } $responseData = curl_exec($curl); $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $responseData = json_decode($responseData, true); if ($statusCode === 0) throw new Exception('cURL Error: ' . curl_error($curl)); curl_close($curl); logModuleCall( 'ArkHostHetznerVPS', $url, !empty($data) ? json_encode($data) : '', print_r($responseData, true) ); // Hetzner API uses standard HTTP status codes if ($statusCode >= 400) { $errorMessage = 'Unknown error'; $errorCode = ''; if (isset($responseData['error'])) { $error = $responseData['error']; $errorMessage = isset($error['message']) ? $error['message'] : $errorMessage; $errorCode = isset($error['code']) ? ' (Code: ' . $error['code'] . ')' : ''; // Add details if available if (isset($error['details'])) { $details = is_array($error['details']) ? json_encode($error['details']) : $error['details']; $errorMessage .= ' - Details: ' . $details; } } throw new Exception($errorMessage . $errorCode); } return $responseData; } function ArkHostHetznerVPS_Error($func, $params, Exception $err) { logModuleCall('ArkHostHetznerVPS', $func, $params, $err->getMessage(), $err->getTraceAsString()); } function ArkHostHetznerVPS_MetaData() { return array( 'DisplayName' => 'ArkHost - HetznerVPS', 'APIVersion' => '1.1', 'RequiresServer' => true, ); } function ArkHostHetznerVPS_ConfigOptions() { $error = array( 'error' => array( 'FriendlyName' => 'Error', 'Description' => 'Please double check if you selected a Server Group and/or your details are correct.', 'Type' => '', ), ); $array = array( 'planid' => array( 'FriendlyName' => 'Server Type', 'Description' => 'The Hetzner server type (Configurable option: planid).', 'Type' => 'dropdown', 'Options' => array(), ), 'osid' => array( 'FriendlyName' => 'Operating System', 'Description' => 'The Operating System image (Configurable option: osid).', 'Type' => 'dropdown', 'Options' => array(), ), 'datacenter' => array( 'FriendlyName' => 'Datacenter', 'Description' => 'The datacenter location for the server.', 'Type' => 'dropdown', 'Options' => array(), ), 'backups' => array( 'FriendlyName' => 'Enable Backups', 'Description' => 'Enable automatic backups (additional cost - billed by Hetzner, ~20% of server price).', 'Type' => 'yesno', ), 'ipv6' => array( 'FriendlyName' => 'Disable IPv6', 'Description' => 'Disable IPv6 for this server.', 'Type' => 'yesno', ), 'floating_ip_location' => array( 'FriendlyName' => 'Floating IP Location', 'Description' => 'Location for floating IPs (same as datacenter)', 'Type' => 'dropdown', 'Options' => array(), ), 'create_floating_ip' => array( 'FriendlyName' => 'Create Floating IP', 'Description' => 'Automatically create a floating IP when provisioning this server (additional cost - billed by Hetzner).', 'Type' => 'yesno', ), 'cloud_init_yaml' => array( 'FriendlyName' => 'Cloud-Init YAML (Optional)', 'Description' => 'Custom cloud-init configuration in YAML format (max 32KiB). Passed as user_data to Hetzner API during server creation. Leave empty to skip cloud-init. Documentation', 'Type' => 'textarea', 'Rows' => '10', 'Cols' => '60', ), ); try { if (basename($_SERVER['SCRIPT_NAME'], '.php') === 'configproducts' && ($_REQUEST['action'] === 'module-settings' || $_POST['action'] === 'module-settings')) { $id = 0; $product = null; $serverGroup = 0; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $id = (int) $_POST['id']; $product = Capsule::table('tblproducts')->where('id', $id)->first(); $serverGroup = (int) $_POST['servergroup']; } else { $id = (int) $_REQUEST['id']; $product = Capsule::table('tblproducts')->where('id', $id)->first(); $serverGroup = (int) $product->servergroup; } // Only proceed if a server group is actually selected if ($serverGroup == 0) { return $array; // Return basic array structure when no server group selected } $serverGroup = Capsule::table('tblservergroupsrel')->where('groupid', $serverGroup)->first(); if (!$serverGroup) { // Return basic array if server group not found, don't throw exception return $array; } $server = Capsule::table('tblservers')->where('id', $serverGroup->serverid)->first(); if (!$server) { // Return basic array if server not found, don't throw exception return $array; } $params = array( 'serverusername' => $server->username, 'serverpassword' => decrypt($server->password), ); $params['action'] = 'Packages'; $packageslist = ArkHostHetznerVPS_API($params); // Hetzner returns server types in 'server_types' array if (isset($packageslist['server_types'])) { foreach ($packageslist['server_types'] as $package) { $price = isset($package['prices'][0]['price_monthly']['gross']) ? number_format($package['prices'][0]['price_monthly']['gross'], 2) : '0.00'; $array['planid']['Options'] += array( $package['name'] => $package['description'] . ' (€' . $price . '/mo)' ); } } if ($product->configoption1 == '') return $array; $params['action'] = 'Operating Systems'; $params['plan_id'] = $product->configoption1; $operatingSystems = ArkHostHetznerVPS_API($params); // Hetzner returns images in 'images' array if (isset($operatingSystems['images'])) { foreach ($operatingSystems['images'] as $operatingSystem) { if ($operatingSystem['type'] === 'system' && $operatingSystem['status'] === 'available') { $array['osid']['Options'] += array( $operatingSystem['name'] => $operatingSystem['description'] ?: $operatingSystem['name'] ); } } } // Fetch datacenters $params['action'] = 'Datacenters'; $datacenters = ArkHostHetznerVPS_API($params); if (isset($datacenters['datacenters'])) { foreach ($datacenters['datacenters'] as $datacenter) { $array['datacenter']['Options'] += array( $datacenter['name'] => $datacenter['description'] . ' (' . $datacenter['location']['city'] . ')' ); // Also populate floating IP locations $array['floating_ip_location']['Options'] += array( $datacenter['location']['name'] => $datacenter['location']['city'] . ', ' . $datacenter['location']['country'] ); } } } } catch (Exception $err) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); // Return the basic array structure with error message instead of error array // This prevents the fields from disappearing return $array; } return $array; } function ArkHostHetznerVPS_GetOption(array $params, $id, $default = NULL) { $options = ArkHostHetznerVPS_ConfigOptions(); $friendlyName = $options[$id]['FriendlyName']; if (isset($params['configoptions'][$friendlyName]) && $params['configoptions'][$friendlyName] !== '') { return $params['configoptions'][$friendlyName]; } else if (isset($params['configoptions'][$id]) && $params['configoptions'][$id] !== '') { return $params['configoptions'][$id]; } else if (isset($params['customfields'][$friendlyName]) && $params['customfields'][$friendlyName] !== '') { return $params['customfields'][$friendlyName]; } else if (isset($params['customfields'][$id]) && $params['customfields'][$id] !== '') { return $params['customfields'][$id]; } $found = false; $i = 0; foreach ($options as $key => $value) { $i++; if ($key === $id) { $found = true; break; } } if ($found && isset($params['configoption' . $i]) && $params['configoption' . $i] !== '') { return $params['configoption' . $i]; } return $default; } function ArkHostHetznerVPS_TestConnection(array $params) { $err = ''; try { $params['action'] = 'Test'; ArkHostHetznerVPS_API($params); } catch (Exception $e) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $e); $err = 'Received the error: ' . $e->getMessage() . ' Check module debug log for more detailed error.'; } return [ 'success' => $err === '', 'error' => $err, ]; } function ArkHostHetznerVPS_CreateAccount(array $params) { try { $params['action'] = 'Order'; $create = ArkHostHetznerVPS_API($params); // Validate Hetzner API response if (!is_array($create) || !isset($create['server']['id'])) { if (is_array($create)) { } throw new Exception('Invalid response from API'); } // Store the server ID (using our format) $params['model']->serviceProperties->save([ 'ArkHostHetznerVPS|VPS ID' => $create['server']['id'], ]); // Store the root password if provided if (isset($create['root_password'])) { Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([ 'password' => encrypt($create['root_password']) ]); // Save the timestamp when password was set for expiration tracking (72 hours) $params['model']->serviceProperties->save([ 'ArkHostHetznerVPS|Password Set Time' => time() ]); } // Handle floating IP creation if requested via Configurable Options or Module Settings $floatingIpType = ArkHostHetznerVPS_GetConfigurableOption($params, 'floating'); $createFloatingIP = ArkHostHetznerVPS_GetOption($params, 'create_floating_ip'); if (($floatingIpType && $floatingIpType !== '' && strtolower($floatingIpType) !== 'none') || ($createFloatingIP && $createFloatingIP === 'on')) { // Determine floating IP type - default to ipv4 if module setting is used $ipType = $floatingIpType && $floatingIpType !== '' ? $floatingIpType : 'ipv4'; try { $floatingIpParams = $params; $floatingIpParams['action'] = 'Create Floating IP'; $floatingIpParams['ip_type'] = $ipType; $floatingIpParams['location'] = ArkHostHetznerVPS_GetOption($params, 'floating_ip_location'); $floatingIpParams['description'] = 'Server ID: ' . $create['server']['id']; $floatingIpParams['assign_to_server'] = true; $floatingIp = ArkHostHetznerVPS_API($floatingIpParams); if (isset($floatingIp['floating_ip']['id'])) { // Store floating IP information $params['model']->serviceProperties->save([ 'ArkHostHetznerVPS|Floating IP ID' => $floatingIp['floating_ip']['id'], 'ArkHostHetznerVPS|Floating IP' => $floatingIp['floating_ip']['ip'], ]); } } catch (Exception $floatingIpErr) { // You might want to send an admin notification here } } } catch (Exception $err) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; } return 'success'; } function ArkHostHetznerVPS_SuspendAccount(array $params) { try { $params['action'] = 'Disable'; ArkHostHetznerVPS_API($params); } catch (Exception $err) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; } return 'success'; } function ArkHostHetznerVPS_UnsuspendAccount(array $params) { try { $params['action'] = 'Enable'; ArkHostHetznerVPS_API($params); } catch (Exception $err) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; } return 'success'; } function ArkHostHetznerVPS_TerminateAccount(array $params) { try { // First, check if there's a floating IP to delete $floatingIpId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID'); if ($floatingIpId) { try { $floatingIpParams = $params; $floatingIpParams['action'] = 'Delete Floating IP'; $floatingIpParams['floating_ip_id'] = $floatingIpId; ArkHostHetznerVPS_API($floatingIpParams); } catch (Exception $floatingIpErr) { } } // Then terminate the server $params['action'] = 'Cancel'; $params['when'] = 'now'; ArkHostHetznerVPS_API($params); Capsule::table('tblhosting')->where('id', $params['serviceid'])->update(array( 'username' => '', 'password' => '', )); } catch (Exception $err) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; } return 'success'; } function ArkHostHetznerVPS_ChangePackage(array $params) { try { $params['action'] = 'Upgrade'; ArkHostHetznerVPS_API($params); } catch (Exception $err) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; } return 'success'; } function ArkHostHetznerVPS_Start(array $params) { try { $params['action'] = 'Start'; ArkHostHetznerVPS_API($params); } catch (Exception $err) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; } return 'success'; } function ArkHostHetznerVPS_Reboot(array $params) { try { $params['action'] = 'Reboot'; ArkHostHetznerVPS_API($params); } catch (Exception $err) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; } return 'success'; } function ArkHostHetznerVPS_Stop(array $params) { try { $params['action'] = 'Stop'; ArkHostHetznerVPS_API($params); } catch (Exception $err) { ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; } return 'success'; } function ArkHostHetznerVPS_VNC(array $params) { try { $params['action'] = 'VNC Console'; $vnc = ArkHostHetznerVPS_API($params); // Hetzner returns wss_url and password if (isset($vnc['wss_url']) && isset($vnc['password'])) { $consoleUrl = $vnc['wss_url']; $vncPassword = $vnc['password']; // Generate a standalone HTML file that can be opened separately $html = '
Unable to open VNC console. Please try again or contact support if the problem persists.
Close Window