From a4106b16d1913181cdd40cfbbd1ea05a6a595504 Mon Sep 17 00:00:00 2001 From: Mason Rogers Date: Sat, 11 Jan 2025 23:28:46 +0000 Subject: [PATCH] additional allocations support --- README.md | 48 +++++- wisp/wisp.php | 470 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 407 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index 11f2c6b..8152168 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + ## WHMCS WHMCS Module for the [WISP Panel](https://wisp.gg/). @@ -5,11 +6,11 @@ WHMCS Module for the [WISP Panel](https://wisp.gg/). Please use the [WISP Discord](https://wisp.gg/discord) for configuration related support instead of GitHub issues. ## Installation -[Video Tutorial](https://www.youtube.com/watch?v=wURpRD9vfj4) +[Video Tutorial](https://www.youtube.com/watch?v=wURpRD9vfj4) -1. Download/Git clone this repository. +1. Download/Git clone this repository. 2. Move the ``wisp/`` folder into ``/modules/servers/``. -3. Create API Credentials with these permissions: ![Image](https://owo.whats-th.is/fa1eee.png) +3. Create API Credentials with these permissions: ![Image](https://i.imgur.com/nzo0u8C.png) 4. In WHMCS navigate to Setup > Products/Services > Servers 5. Create new server, fill the name with anything you want, hostname as the url to the panel. For example: ``my-panel.panel.gg`` 6. Change Server Type to WISP, leave username empty, fill the password field with your generated API Key. @@ -24,6 +25,7 @@ Please use the [WISP Discord](https://wisp.gg/discord) for configuration related ## Credits [Dane](https://github.com/DaneEveritt) and [everyone else](https://github.com/Pterodactyl/Panel/graphs/contributors) involved in development of the Pterodactyl Panel. [death-droid](https://github.com/death-droid) for the original WHMCS module. +[snags141](https://github.com/snags141/) for additional allocation support. # FAQ @@ -31,17 +33,17 @@ Please use the [WISP Discord](https://wisp.gg/discord) for configuration related This module is backwards compatible and requires no changing other than switching to this module. ## Overwriting values through configurable options -Overwriting values can be done through either Configurable Options or Custom Fields. +Overwriting values can be done through either Configurable Options or Custom Fields. Their name should be exactly what you want to overwrite. dedicated_ip => Will overwrite dedicated_ip if its ticked or not. -Valid options: ``server_name, memory, swap, io, cpu, disk, nest_id, egg_id, pack_id, location_id, dedicated_ip, port_range, image, startup, databases, allocations, oom_disabled, force_outgoing_ip, username, backup_megabytes_limit`` +Valid options: ``server_name, memory, swap, io, cpu, disk, nest_id, egg_id, pack_id, location_id, dedicated_ip, port_range, image, startup, databases, allocations, oom_disabled, username, backup_megabytes_limit`` This also works for any name of environment variable: -Player Slots => Will overwrite the environment variable named "Player Slots" to its value. +Player Slots => Will overwrite the environment variable named "Player Slots" to its value. -Useful trick: You can use the | seperator to change the display name of the variable like this: -dedicated_ip|Dedicated IP => Will be displayed as "Dedicated IP" but will work correctly. +Useful trick: You can use the | separator to change the display name of the variable like this: +dedicated_ip|Dedicated IP => Will be displayed as "Dedicated IP" but will work correctly. [Sample configuration for configurable memory](https://owo.whats-th.is/85JwhVX.png) @@ -52,7 +54,35 @@ This can be caused from any of the following: Wrong location, not enough disk sp The customer gets an email from the panel to setup their account (incl. password) if they didn't have an account before. Otherwise they should be able to use their existing credentials. ## My game requires multiple ports allocated. -Currently, this isn't possible with this module but is planned. +Configure the "**Additional Ports**" option in the module settings. +It expects a valid JSON string consisting of the parameter name or NONE, and a number representing a port as a numeric offset from the first available allocation. +E.g: If you enter `{"1":"NONE", "2":"NONE", "4":"NONE"}` and the first available port happens to be 25565, you'll get as additional allocations: +* 25566 (First Port + 1) +* 25567 (First Port +2) +* 25569 (First Port +4) + +(If they're available) + +Note: I this option is set, it will override anything specified under "port_range" - Use one or the other, not both. + +You'll also want to configure "**Additional Port Failure Mode**". +This determines what the module should do if there are no allocations available on any of the defined nodes. +* "Continue" - Continues creating the server but only with one allocation, whatever is available at the time. You'll need to manually go in after the server gets created to assign additional ports as required. +* "Stop" - Stops the server creation and raises an error. + +## How to assign additional allocations to server parameters like RCON_PORT +See the table below for "Additional Ports" example values. +*These examples assume your WISP node has allocations available from 1000-2000.* + +| Game | Required Ports |Additional Ports Example | Ports Assigned | +| ------------ | ------------ | ------------ | ------------ | +| Rust | Game port and RCON port | `{"1":"RCON_PORT"}` | Game Port: 1000, RCON_PORT: 1001| +| Arma 3 | Game port, Game port +1 for Steam Query, Game port + 2 for Steam Port, and Game port +4 for BattleEye | `{"1":"NONE", "2":"NONE", "4":"NONE"}` | Game Port: 1000, Additional Ports: 1001, 1002, 1004 | +| Unturned | Game port, Game port +1 and Game port +2 | `{"1":"NONE", "2":"NONE"}` | Game Port: 1000, Additional Ports: 1001, 1002 | +| Project Zomboid | Game Port, Steam port and an additional port for every player. Let's say we want 10 additional ports for 10 players. | `{"1":"STEAM_PORT", 2":"NONE", "3":"NONE", "4":"NONE", "5":"NONE", "6":"NONE", "7":"NONE", "8":"NONE", "9":"NONE", "10":"NONE", "11":"NONE"}` | Game Port: 1000, Steam Port: 1001, Additional Ports: 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011| + +**What does "NONE" mean?** +"NONE" means you want to assign the additional port to the server, but it doesn't need to be assigned to a server parameter. If instead you want to add a +1 port and assign it to the parameter "RCON_PORT" then you'd use `{"1":"RCON_PORT"}` for example. ## How to enable module debug log 1. In WHMCS 7 or below navigate to Utilities > Logs > Module Log. For WHMCS 8.x navigate to System Logs > Module Log in the left sidebar. diff --git a/wisp/wisp.php b/wisp/wisp.php index a620e2b..c16ebc8 100644 --- a/wisp/wisp.php +++ b/wisp/wisp.php @@ -1,3 +1,4 @@ + dash, etc) in some cases - foreach([ - 'DOT' => '.', - 'DASH' => '-', - ] as $from => $to) { + foreach ([ + 'DOT' => '.', + 'DASH' => '-', + ] as $from => $to) { $hostname = str_replace($from, $to, $hostname); } - if(ip2long($hostname) !== false) $hostname = 'http://' . $hostname; + if (ip2long($hostname) !== false) $hostname = 'http://' . $hostname; else $hostname = ($params['serversecure'] ? 'https://' : 'http://') . $hostname; return rtrim($hostname, '/'); } -function wisp_API(array $params, $endpoint, array $data = [], $method = "GET", $dontLog = false) { +function wisp_API(array $params, $endpoint, array $data = [], $method = "GET", $dontLog = false) +{ $url = wisp_GetHostname($params) . '/api/application/' . $endpoint; $curl = curl_init(); @@ -66,7 +69,7 @@ function wisp_API(array $params, $endpoint, array $data = [], $method = "GET", $ "Accept: Application/vnd.wisp.v1+json", ]; - if($method === 'POST' || $method === 'PATCH') { + if ($method === 'POST' || $method === 'PATCH') { $jsonData = json_encode($data); curl_setopt($curl, CURLOPT_POSTFIELDS, $jsonData); array_push($headers, "Content-Type: application/json"); @@ -78,23 +81,28 @@ function wisp_API(array $params, $endpoint, array $data = [], $method = "GET", $ $response = curl_exec($curl); $responseData = json_decode($response, true); $responseData['status_code'] = curl_getinfo($curl, CURLINFO_HTTP_CODE); - - if($responseData['status_code'] === 0 && !$dontLog) logModuleCall("WISP-WHMCS", "CURL ERROR", curl_error($curl), ""); + + if ($responseData['status_code'] === 0 && !$dontLog) logModuleCall("WISP-WHMCS", "CURL ERROR", curl_error($curl), ""); curl_close($curl); - if(!$dontLog) logModuleCall("WISP-WHMCS", $method . " - " . $url, + if (!$dontLog) logModuleCall( + "WISP-WHMCS", + $method . " - " . $url, isset($data) ? json_encode($data) : "", - print_r($responseData, true)); + print_r($responseData, true) + ); return $responseData; } -function wisp_Error($func, $params, Exception $err) { +function wisp_Error($func, $params, Exception $err) +{ logModuleCall("WISP-WHMCS", $func, $params, $err->getMessage(), $err->getTraceAsString()); } -function wisp_MetaData() { +function wisp_MetaData() +{ return [ "DisplayName" => "WISP", "APIVersion" => "1.1", @@ -102,7 +110,8 @@ function wisp_MetaData() { ]; } -function wisp_ConfigOptions() { +function wisp_ConfigOptions() +{ return [ "cpu" => [ "FriendlyName" => "CPU Limit (%)", @@ -188,7 +197,7 @@ function wisp_ConfigOptions() { "Type" => "text", "Size" => 10, ], - "server_name" => [ + "server_name" => [ "FriendlyName" => "Server Name", "Description" => "The name of the server as shown on the panel (optional)", "Type" => "text", @@ -199,21 +208,33 @@ function wisp_ConfigOptions() { "Description" => "Should the Out Of Memory Killer be disabled (optional)", "Type" => "yesno", ], - "force_outgoing_ip" => [ - "FriendlyName" => "Force Outgoing IP", - "Description" => "Forces the server to use the primary allocation's IP for outgoing connections by creating a separate Docker network. Required by some games to work properly with multiple IPs on the machine, such as Source Engine games (optional)", - "Type" => "yesno", - ], "backup_megabytes_limit" => [ "FriendlyName" => "Backup Size Limit", "Description" => "Amount in megabytes the server can use for backups (optional)", "Type" => "text", "Size" => 25, ], + "additional_ports" => [ + "FriendlyName" => "Additional Ports", + "Description" => "Additional ports to assign to the server. See the module readme for instructions: View Readme (optional)", + "Type" => "text", + "Size" => 25, + ], + "additional_port_fail_mode" => [ + "FriendlyName" => "Additional Port Failure Mode", + "Type" => "dropdown", + "Options" => [ + 'continue' => 'Continue', + 'stop' => 'Stop', + ], + "Description" => "Determines whether server creation will continue if none of your nodes are able to satisfy the additional port allocation. See the module readme for more information: View Readme", + "Default" => "continue", + ], ]; } -function wisp_TestConnection(array $params) { +function wisp_TestConnection(array $params) +{ $solutions = [ 0 => "Check module debug log for more detailed error.", 401 => "Authorization header either missing or not provided.", @@ -227,16 +248,16 @@ function wisp_TestConnection(array $params) { try { $response = wisp_API($params, 'nodes'); - if($response['status_code'] !== 200) { + if ($response['status_code'] !== 200) { $status_code = $response['status_code']; $err = "Invalid status_code received: " . $status_code . ". Possible solutions: " . (isset($solutions[$status_code]) ? $solutions[$status_code] : "None."); } else { - if($response['meta']['pagination']['count'] === 0) { + if ($response['meta']['pagination']['count'] === 0) { $err = "Authentication successful, but no nodes are available."; } } - } catch(Exception $e) { + } catch (Exception $e) { wisp_Error(__FUNCTION__, $params, $e); $err = $e->getMessage(); } @@ -247,46 +268,50 @@ function wisp_TestConnection(array $params) { ]; } -function wisp_GetOption(array $params, $id, $default = NULL) { +function wisp_GetOption(array $params, $id, $default = NULL) +{ $options = wisp_ConfigOptions(); $friendlyName = $options[$id]['FriendlyName']; - if(isset($params['configoptions'][$friendlyName]) && $params['configoptions'][$friendlyName] !== '') { + if (isset($params['configoptions'][$friendlyName]) && $params['configoptions'][$friendlyName] !== '') { return $params['configoptions'][$friendlyName]; - } else if(isset($params['configoptions'][$id]) && $params['configoptions'][$id] !== '') { + } else if (isset($params['configoptions'][$id]) && $params['configoptions'][$id] !== '') { return $params['configoptions'][$id]; - } else if(isset($params['customfields'][$friendlyName]) && $params['customfields'][$friendlyName] !== '') { + } else if (isset($params['customfields'][$friendlyName]) && $params['customfields'][$friendlyName] !== '') { return $params['customfields'][$friendlyName]; - } else if(isset($params['customfields'][$id]) && $params['customfields'][$id] !== '') { + } else if (isset($params['customfields'][$id]) && $params['customfields'][$id] !== '') { return $params['customfields'][$id]; } $found = false; $i = 0; - foreach(wisp_ConfigOptions() as $key => $value) { + foreach (wisp_ConfigOptions() as $key => $value) { $i++; - if($key === $id) { + if ($key === $id) { $found = true; break; } } - if($found && isset($params['configoption' . $i]) && $params['configoption' . $i] !== '') { + if ($found && isset($params['configoption' . $i]) && $params['configoption' . $i] !== '') { return $params['configoption' . $i]; } return $default; } -function wisp_CreateAccount(array $params) { +function wisp_CreateAccount(array $params) +{ try { + // Checking if the server ID already exists $serverId = wisp_GetServerID($params); - if(isset($serverId)) throw new Exception('Failed to create server because it is already created.'); + if (isset($serverId)) throw new Exception('Failed to create server because it is already created.'); + // Create or fetch the user account $userResult = wisp_API($params, 'users/external/' . $params['clientsdetails']['uuid']); - if($userResult['status_code'] === 404) { + if ($userResult['status_code'] === 404) { $userResult = wisp_API($params, 'users?search=' . urlencode($params['clientsdetails']['email'])); - if($userResult['meta']['pagination']['total'] === 0) { + if ($userResult['meta']['pagination']['total'] === 0) { $userResult = wisp_API($params, 'users', [ 'email' => $params['clientsdetails']['email'], 'first_name' => empty($params['clientsdetails']['firstname']) ? 'Unknown' : $params['clientsdetails']['firstname'], @@ -294,8 +319,8 @@ function wisp_CreateAccount(array $params) { 'external_id' => $params['clientsdetails']['uuid'], ], 'POST'); } else { - foreach($userResult['data'] as $key => $value) { - if($value['attributes']['email'] === $params['clientsdetails']['email']) { + foreach ($userResult['data'] as $key => $value) { + if ($value['attributes']['email'] === $params['clientsdetails']['email']) { $userResult = array_merge($userResult, $value); break; } @@ -304,31 +329,33 @@ function wisp_CreateAccount(array $params) { } } - if($userResult['status_code'] === 200 || $userResult['status_code'] === 201) { + if ($userResult['status_code'] === 200 || $userResult['status_code'] === 201) { $userId = $userResult['attributes']['id']; } else { throw new Exception('Failed to create user, received error code: ' . $userResult['status_code'] . '. Enable module debug log for more info.'); } + // Get egg data $nestId = wisp_GetOption($params, 'nest_id'); $eggId = wisp_GetOption($params, 'egg_id'); $eggData = wisp_API($params, 'nests/' . $nestId . '/eggs/' . $eggId . '?include=variables'); - if($eggData['status_code'] !== 200) throw new Exception('Failed to get egg data, received error code: ' . $eggData['status_code'] . '. Enable module debug log for more info.'); + if ($eggData['status_code'] !== 200) throw new Exception('Failed to get egg data, received error code: ' . $eggData['status_code'] . '. Enable module debug log for more info.'); $environment = []; - foreach($eggData['attributes']['relationships']['variables']['data'] as $key => $val) { + foreach ($eggData['attributes']['relationships']['variables']['data'] as $key => $val) { $attr = $val['attributes']; $var = $attr['env_variable']; $default = $attr['default_value']; $friendlyName = wisp_GetOption($params, $attr['name']); $envName = wisp_GetOption($params, $attr['env_variable']); - if(isset($friendlyName)) $environment[$var] = $friendlyName; - elseif(isset($envName)) $environment[$var] = $envName; + if (isset($friendlyName)) $environment[$var] = $friendlyName; + elseif (isset($envName)) $environment[$var] = $envName; else $environment[$var] = $default; } + // Fetch given server parameters $name = wisp_GetOption($params, 'server_name', 'My Server'); $memory = wisp_GetOption($params, 'memory'); $swap = wisp_GetOption($params, 'swap'); @@ -339,13 +366,14 @@ function wisp_CreateAccount(array $params) { $location_id = wisp_GetOption($params, 'location_id'); $dedicated_ip = wisp_GetOption($params, 'dedicated_ip') ? true : false; $port_range = wisp_GetOption($params, 'port_range'); + $additional_ports = wisp_GetOption($params, 'additional_ports'); + $additional_port_fail_mode = wisp_GetOption($params, 'additional_port_fail_mode'); $port_range = isset($port_range) ? explode(',', $port_range) : []; $image = wisp_GetOption($params, 'image', $eggData['attributes']['docker_image']); $startup = wisp_GetOption($params, 'startup', $eggData['attributes']['startup']); $databases = wisp_GetOption($params, 'databases'); $allocations = wisp_GetOption($params, 'allocations'); - $oom_disabled = wisp_GetOption($params, 'oom_disabled') ? true : false; - $force_outgoing_ip = wisp_GetOption($params, 'force_outgoing_ip') ? true : false; + $oom_disabled = wisp_GetOption($params, 'oom_disabled') == 'yes'; $backup_megabytes_limit = wisp_GetOption($params, 'backup_megabytes_limit'); $serverData = [ 'name' => $name, @@ -355,7 +383,6 @@ function wisp_CreateAccount(array $params) { 'docker_image' => $image, 'startup' => $startup, 'oom_disabled' => $oom_disabled, - 'force_outgoing_ip' => $force_outgoing_ip, 'limits' => [ 'memory' => (int) $memory, 'swap' => (int) $swap, @@ -377,19 +404,89 @@ function wisp_CreateAccount(array $params) { 'start_on_completion' => true, 'external_id' => (string) $params['serviceid'], ]; - if(isset($pack_id)) $serverData['pack'] = (int) $pack_id; + if (isset($pack_id)) $serverData['pack'] = (int) $pack_id; + + // Check if additional ports have been set + if (isset($additional_ports) && $additional_ports != '') { + + // Query all nodes for the given location until we find an available set of ports + // Get the list of additional ports to add + //$additional_port_list = explode(",", $additional_ports); + $additional_port_list = $additional_ports; + // Get the server nodes for the specified location_id + $nodes = getPaginatedData($params, 'locations/' . $location_id . '/eligible-nodes'); + + // Get the port allocations for each node at this location and check if there's space for the additional ports + if (isset($nodes)) { + logModuleCall("WISP-WHMCS", "Got " . count($nodes) . " possibly eligible nodes for deployment"); + + $alloc_success = false; + foreach ($nodes as $key => $node_id) { + logModuleCall("WISP-WHMCS", "Checking allocations for node: " . $node_id, "", ""); + + // Get all the available allocations for this node + $available_allocations = getAllocations($params, $node_id); + + // Taking our additional allocation requirements and available node allocations, find a combination of available ports. + $final_allocations = findFreePorts($available_allocations, $additional_port_list, $serverData['deploy']); + + if ($final_allocations != false && $final_allocations['status'] == true) { + $alloc_success = true; + logModuleCall("WISP-WHMCS", "Successfully found an allocation. Setting primary allocation to ID " . $final_allocations['main_allocation_id'], "", ""); + unset($serverData['deploy']); + $serverData['allocation']['default'] = intval($final_allocations['main_allocation_id']); + $serverData['allocation']['additional'] = $final_allocations['additional_allocation_ids']; + + // Update the environment parameters - additional allocations + foreach ($final_allocations['additional_allocation_ports'] as $key => $port) { + // If the key given in the config had a value of NONE, don't worry about adding it to the environment parameters. + if (substr($key, 0, 4) !== "NONE") { + $serverData['environment'][$key] = $port; + } + } + // We successfully found and assigned an available allocation, break and check no more nodes. + break; + } + logModuleCall("WISP-WHMCS", "Failed to find an available allocation on node: " . $node_id, "", ""); + } + if (!$alloc_success) { + // Failure handling logic + if ($additional_port_fail_mode == "stop") { + throw new Exception('Couldn\'t find any nodes to satisfy the requested allocations.'); + } else { + // Continue with normal deployment + $serverData['deploy']['port_range'] = $port_range; + } + } + } else { + logModuleCall("WISP-WHMCS", "Unable to find any nodes at location ID: " . $location_id, "", ""); + throw new Exception('Couldn\'t find any nodes satisfying the request at location: ' . $location_id); + } + } else { + // Continue with normal deployment + $serverData['deploy']['port_range'] = $port_range; + } + + logModuleCall("WISP-WHMCS", "Create Account", print_r($serverData, true), ""); + logModuleCall("WISP-WHMCS", "Create Account", print_r($params, true), ""); + // Create the game server $server = wisp_API($params, 'servers', $serverData, 'POST'); - if($server['status_code'] === 400) throw new Exception('Couldn\'t find any nodes satisfying the request.'); - if($server['status_code'] !== 201) throw new Exception('Failed to create the server, received the error code: ' . $server['status_code'] . '. Enable module debug log for more info.'); + // Catch API errors + if ($server['status_code'] === 400) throw new Exception('Couldn\'t find any nodes satisfying the request.'); + if ($server['status_code'] !== 201 && $server['status_code'] !== 200) throw new Exception('Failed to create the server, received the error code: ' . $server['status_code'] . '. Enable module debug log for more info.'); + if (isset($server['errors']) && count($server['errors']) > 0) { + $error = $server['errors'][0]; + throw new Exception('Failed to create the server, received the error: ' . $error['detail'] . '. Enable module debug log for more info.'); + } unset($params['password']); Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([ 'username' => '', 'password' => '', ]); - } catch(Exception $err) { + } catch (Exception $err) { return $err->getMessage(); } @@ -397,25 +494,26 @@ function wisp_CreateAccount(array $params) { } // Function to allow backwards compatibility with death-droid's module -function wisp_GetServerID(array $params, $raw = false) { +function wisp_GetServerID(array $params, $raw = false) +{ $serverResult = wisp_API($params, 'servers/external/' . $params['serviceid'], [], 'GET', true); - if($serverResult['status_code'] === 200) { - if($raw) return $serverResult; + if ($serverResult['status_code'] === 200) { + if ($raw) return $serverResult; else return $serverResult['attributes']['id']; - } else if($serverResult['status_code'] === 500) { + } else if ($serverResult['status_code'] === 500) { throw new Exception('Failed to get server, panel errored. Check panel logs for more info.'); } - if(Capsule::schema()->hasTable('tbl_pterodactylproduct')) { + if (Capsule::schema()->hasTable('tbl_pterodactylproduct')) { $oldData = Capsule::table('tbl_pterodactylproduct') ->select('user_id', 'server_id') ->where('service_id', '=', $params['serviceid']) ->first(); - if(isset($oldData) && isset($oldData->server_id)) { - if($raw) { + if (isset($oldData) && isset($oldData->server_id)) { + if ($raw) { $serverResult = wisp_API($params, 'servers/' . $oldData->server_id); - if($serverResult['status_code'] === 200) return $serverResult; + if ($serverResult['status_code'] === 200) return $serverResult; else throw new Exception('Failed to get server, received the error code: ' . $serverResult['status_code'] . '. Enable module debug log for more info.'); } else { return $oldData->server_id; @@ -424,58 +522,62 @@ function wisp_GetServerID(array $params, $raw = false) { } } -function wisp_SuspendAccount(array $params) { +function wisp_SuspendAccount(array $params) +{ try { $serverId = wisp_GetServerID($params); - if(!isset($serverId)) throw new Exception('Failed to suspend server because it doesn\'t exist.'); + if (!isset($serverId)) throw new Exception('Failed to suspend server because it doesn\'t exist.'); $suspendResult = wisp_API($params, 'servers/' . $serverId . '/suspend', [], 'POST'); - if($suspendResult['status_code'] !== 204) throw new Exception('Failed to suspend the server, received error code: ' . $suspendResult['status_code'] . '. Enable module debug log for more info.'); - } catch(Exception $err) { + if ($suspendResult['status_code'] !== 204) throw new Exception('Failed to suspend the server, received error code: ' . $suspendResult['status_code'] . '. Enable module debug log for more info.'); + } catch (Exception $err) { return $err->getMessage(); } return 'success'; } -function wisp_UnsuspendAccount(array $params) { +function wisp_UnsuspendAccount(array $params) +{ try { $serverId = wisp_GetServerID($params); - if(!isset($serverId)) throw new Exception('Failed to unsuspend server because it doesn\'t exist.'); + if (!isset($serverId)) throw new Exception('Failed to unsuspend server because it doesn\'t exist.'); $suspendResult = wisp_API($params, 'servers/' . $serverId . '/unsuspend', [], 'POST'); - if($suspendResult['status_code'] !== 204) throw new Exception('Failed to unsuspend the server, received error code: ' . $suspendResult['status_code'] . '. Enable module debug log for more info.'); - } catch(Exception $err) { + if ($suspendResult['status_code'] !== 204) throw new Exception('Failed to unsuspend the server, received error code: ' . $suspendResult['status_code'] . '. Enable module debug log for more info.'); + } catch (Exception $err) { return $err->getMessage(); } return 'success'; } -function wisp_TerminateAccount(array $params) { +function wisp_TerminateAccount(array $params) +{ try { $serverId = wisp_GetServerID($params); - if(!isset($serverId)) throw new Exception('Failed to terminate server because it doesn\'t exist.'); + if (!isset($serverId)) throw new Exception('Failed to terminate server because it doesn\'t exist.'); $deleteResult = wisp_API($params, 'servers/' . $serverId, [], 'DELETE'); - if($deleteResult['status_code'] !== 204) throw new Exception('Failed to terminate the server, received error code: ' . $deleteResult['status_code'] . '. Enable module debug log for more info.'); - } catch(Exception $err) { + if ($deleteResult['status_code'] !== 204) throw new Exception('Failed to terminate the server, received error code: ' . $deleteResult['status_code'] . '. Enable module debug log for more info.'); + } catch (Exception $err) { return $err->getMessage(); } return 'success'; } -function wisp_ChangePassword(array $params) { +function wisp_ChangePassword(array $params) +{ try { - if($params['password'] === '') throw new Exception('The password cannot be empty.'); + if ($params['password'] === '') throw new Exception('The password cannot be empty.'); $serverData = wisp_GetServerID($params, true); - if(!isset($serverData)) throw new Exception('Failed to change password because linked server doesn\'t exist.'); + if (!isset($serverData)) throw new Exception('Failed to change password because linked server doesn\'t exist.'); $userId = $serverData['attributes']['user']; $userResult = wisp_API($params, 'users/' . $userId); - if($userResult['status_code'] !== 200) throw new Exception('Failed to retrieve user, received error code: ' . $userResult['status_code'] . '.'); + if ($userResult['status_code'] !== 200) throw new Exception('Failed to retrieve user, received error code: ' . $userResult['status_code'] . '.'); $updateResult = wisp_API($params, 'users/' . $serverData['attributes']['user'], [ 'email' => $userResult['attributes']['email'], @@ -484,24 +586,25 @@ function wisp_ChangePassword(array $params) { 'password' => $params['password'], ], 'PATCH'); - if($updateResult['status_code'] !== 200) throw new Exception('Failed to change password, received error code: ' . $updateResult['status_code'] . '.'); + if ($updateResult['status_code'] !== 200) throw new Exception('Failed to change password, received error code: ' . $updateResult['status_code'] . '.'); unset($params['password']); Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([ 'username' => '', 'password' => '', ]); - } catch(Exception $err) { + } catch (Exception $err) { return $err->getMessage(); } return 'success'; } -function wisp_ChangePackage(array $params) { +function wisp_ChangePackage(array $params) +{ try { $serverData = wisp_GetServerID($params, true); - if($serverData['status_code'] === 404 || !isset($serverData['attributes']['id'])) throw new Exception('Failed to change package of server because it doesn\'t exist.'); + if ($serverData['status_code'] === 404 || !isset($serverData['attributes']['id'])) throw new Exception('Failed to change package of server because it doesn\'t exist.'); $serverId = $serverData['attributes']['id']; $memory = wisp_GetOption($params, 'memory'); @@ -511,8 +614,7 @@ function wisp_ChangePackage(array $params) { $disk = wisp_GetOption($params, 'disk'); $databases = wisp_GetOption($params, 'databases'); $allocations = wisp_GetOption($params, 'allocations'); - $oom_disabled = wisp_GetOption($params, 'oom_disabled') ? true : false; - $force_outgoing_ip = wisp_GetOption($params, 'force_outgoing_ip') ? true : false; + $oom_disabled = wisp_GetOption($params, 'oom_disabled') == 'yes'; $backup_megabytes_limit = wisp_GetOption($params, 'backup_megabytes_limit'); $updateData = [ 'allocation' => $serverData['attributes']['allocation'], @@ -522,7 +624,6 @@ function wisp_ChangePackage(array $params) { 'cpu' => (int) $cpu, 'disk' => (int) $disk, 'oom_disabled' => $oom_disabled, - 'force_outgoing_ip' => $force_outgoing_ip, 'feature_limits' => [ 'databases' => (int) $databases, 'allocations' => (int) $allocations, @@ -530,26 +631,29 @@ function wisp_ChangePackage(array $params) { ], ]; + // log to the module log + logModuleCall("WISP-WHMCS", "Change Package", print_r($updateData, true), ""); + $updateResult = wisp_API($params, 'servers/' . $serverId . '/build', $updateData, 'PATCH'); - if($updateResult['status_code'] !== 200) throw new Exception('Failed to update build of the server, received error code: ' . $updateResult['status_code'] . '. Enable module debug log for more info.'); + if ($updateResult['status_code'] !== 200) throw new Exception('Failed to update build of the server, received error code: ' . $updateResult['status_code'] . '. Enable module debug log for more info.'); $nestId = wisp_GetOption($params, 'nest_id'); $eggId = wisp_GetOption($params, 'egg_id'); $pack_id = wisp_GetOption($params, 'pack_id'); $eggData = wisp_API($params, 'nests/' . $nestId . '/eggs/' . $eggId . '?include=variables'); - if($eggData['status_code'] !== 200) throw new Exception('Failed to get egg data, received error code: ' . $eggData['status_code'] . '. Enable module debug log for more info.'); + if ($eggData['status_code'] !== 200) throw new Exception('Failed to get egg data, received error code: ' . $eggData['status_code'] . '. Enable module debug log for more info.'); $environment = []; - foreach($eggData['attributes']['relationships']['variables']['data'] as $key => $val) { + foreach ($eggData['attributes']['relationships']['variables']['data'] as $key => $val) { $attr = $val['attributes']; $var = $attr['env_variable']; $friendlyName = wisp_GetOption($params, $attr['name']); $envName = wisp_GetOption($params, $attr['env_variable']); - if(isset($friendlyName)) $environment[$var] = $friendlyName; - elseif(isset($envName)) $environment[$var] = $envName; - elseif(isset($serverData['attributes']['container']['environment'][$var])) $environment[$var] = $serverData['attributes']['container']['environment'][$var]; - elseif(isset($attr['default_value'])) $environment[$var] = $attr['default_value']; + if (isset($friendlyName)) $environment[$var] = $friendlyName; + elseif (isset($envName)) $environment[$var] = $envName; + elseif (isset($serverData['attributes']['container']['environment'][$var])) $environment[$var] = $serverData['attributes']['container']['environment'][$var]; + elseif (isset($attr['default_value'])) $environment[$var] = $attr['default_value']; } $image = wisp_GetOption($params, 'image', $serverData['attributes']['container']['image']); @@ -564,35 +668,36 @@ function wisp_ChangePackage(array $params) { ]; $updateResult = wisp_API($params, 'servers/' . $serverId . '/startup', $updateData, 'PATCH'); - if($updateResult['status_code'] !== 200) throw new Exception('Failed to update startup of the server, received error code: ' . $updateResult['status_code'] . '. Enable module debug log for more info.'); - } catch(Exception $err) { + if ($updateResult['status_code'] !== 200) throw new Exception('Failed to update startup of the server, received error code: ' . $updateResult['status_code'] . '. Enable module debug log for more info.'); + } catch (Exception $err) { return $err->getMessage(); } return 'success'; } -function wisp_LoginLink(array $params) { - if($params['moduletype'] !== 'wisp') return; +function wisp_LoginLink(array $params) +{ + if ($params['moduletype'] !== 'wisp') return; try { $serverId = wisp_GetServerID($params); - if(!isset($serverId)) return; + if (!isset($serverId)) return; $hostname = wisp_GetHostname($params); - echo '[Go to Service]'; - echo '

[Report A Bug]

'; - } catch(Exception $err) { + echo '[Go to Service]'; + } catch (Exception $err) { // Ignore } } -function wisp_ClientArea(array $params) { - if($params['moduletype'] !== 'wisp') return; +function wisp_ClientArea(array $params) +{ + if ($params['moduletype'] !== 'wisp') return; try { $serverData = wisp_GetServerID($params, true); - if($serverData['status_code'] === 404 || !isset($serverData['attributes']['id'])) return; + if ($serverData['status_code'] === 404 || !isset($serverData['attributes']['id'])) return; $hostname = wisp_GetHostname($params); @@ -606,3 +711,164 @@ function wisp_ClientArea(array $params) { // Ignore } } + +/* Utility Functions */ + +/** + * Gets the available allocations for a specific node_id + * and returns them in a format that can be more easily parsed. + * Output: + * Returns the available allocations in + * a more usable format for filtering + * Format: + * [ + * [] => { + * [] => [ + * ['id'] = 1234; + * ] + * }, + * ['192.168.1.123'] => [ + * ['1234'] => [ + * ['id'] = 1234; + * ] + * ], + * ] + */ +function getAllocations(array $params, int $node_id) +{ + $allocation_ids = array(); + + $allocations = getPaginatedData($params, 'nodes/' . $node_id . '/allocations/available'); + + foreach ($allocations as $key => $allocation) { + $ip = $allocation['attributes']['ip']; + $port = $allocation['attributes']['port']; + + $allocation_ids[$ip][$port]['id'] = $allocation['attributes']['id']; + } + + return $allocation_ids; +} + +// Makes a paginated API request and returns the response. +function getPaginatedData($params, $url) +{ + $results = array(); + + // Fetch and parse first page of data + $response = wisp_API($params, $url); + + foreach ($response['data'] as $key => $value) { + $results[] = $value; + } + + // Fetch and parse any remaining pages + $current_page = $response['meta']['pagination']['current_page']; + $total_pages = $response['meta']['pagination']['total_pages']; + while ($total_pages > $current_page) { + $next_page = intval($current_page) + 1; + + $response = wisp_API($params, $url . '?page=' . $next_page); + + foreach ($response['data'] as $key => $value) { + $results[] = $value; + } + + $current_page = $response['meta']['pagination']['current_page']; + $total_pages = $response['meta']['pagination']['total_pages']; + } + return $results; +} + +function findFreePorts(array $available_allocations, string $port_offsets, $deploy) +{ + /* + This is the main logic that takes a list of available allocations + and the required offsets and then finds the first available set. + e.g. if port offsets +1 +2 and +4 are requested (format: 1,2,4) + we take each port one by one and check if + 1, + + 2 and + 4 are available. + If all requested port allocations are available, they are returned. + + Inputs: + $available_allocations This is the first port in the range to test. + All other ports will be checked based on the + required offset from the first. + + $port_offsets The json string of offsets from the first port that + are required for the server. + + Outputs: + $ports_found The array of ports that were found available, based on the offsets + the additional ports required. + */ + + $port_offsets_array = json_decode($port_offsets, true); + + // Iterate over available IP's + foreach ($available_allocations as $ip_addr => $ports) { + $result = array(); + $result['status'] = false; + $main_allocation_id = ""; + $main_allocation_port = ""; + $additional_allocation_ids = array(); + $additional_allocation_ports = array(); + if (!empty($deploy["port_range"])) { + $deploy["port_range"] = json_encode($deploy["port_range"]); + //converts port_range string to object filled with int + $portrange = []; + array_push($portrange, intval(ltrim(strstr($deploy["port_range"], '-', true), '["'))); + array_push($portrange, intval(ltrim(strstr($deploy["port_range"], '-'), '-'))); + + for ($i = $portrange[0] + 1; $i < $portrange[1]; $i++) { + array_push($portrange, $i); + } + //no need to sort but doing it to make it easier for possible future features + sort($portrange); + foreach ($ports as $port => $portDetails) { + json_decode($port); + if (!in_array($port, $portrange)) { + unset($ports[$port]); + } + } + } + // Iterate over Ports + logModuleCall("WISP-WHMCS", "Checking IP: " . $ip_addr, "", ""); + foreach ($ports as $port => $portDetails) { + $main_allocation_id = $portDetails['id']; + $main_allocation_port = $port; + $found_all = true; + foreach ($port_offsets_array as $port_offset => $environment) { + $next_port = intval($port) + intval($port_offset); + if (!isset($ports[$next_port])) { + // Port is not available + $found_all = false; + } else { + // Port is available, add it to the array + array_push($additional_allocation_ids, strval($ports[$next_port]['id'])); + //array_push($additional_allocation_ports, strval($next_port)); + + $additional_allocation_ports[$environment] = $next_port; + } + } + if ($found_all == true) { + logModuleCall("WISP-WHMCS", "Found a game port allocation ID: " . $main_allocation_id, "", ""); + logModuleCall("WISP-WHMCS", "Found additional allocation ID's: " . print_r($additional_allocation_ids, true), "", ""); + logModuleCall("WISP-WHMCS", "Found additional allocation Ports: " . print_r($additional_allocation_ports, true), "", ""); + $result['main_allocation_id'] = $main_allocation_id; + $result['main_allocation_port'] = $main_allocation_port; + $result['additional_allocation_ids'] = $additional_allocation_ids; + $result['additional_allocation_ports'] = $additional_allocation_ports; + $result['status'] = true; + return $result; + } else { + // Reset values in array for next run + $additional_allocation_ids = array(); + $additional_allocation_ports = array(); + } + } + } + // Failed to find available set of ports based on requirements + logModuleCall("WISP-WHMCS", "Failed to find available ports!", "", ""); + return false; +}