From b6d25a4e2219569352ebb28fbb07700e4569e2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8B=BF=E5=BF=98=E5=BF=83=E5=AE=89?= <36999228+xkatld@users.noreply.github.com> Date: Thu, 20 Nov 2025 05:52:25 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0whmcs=E6=8F=92?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- whmcs/lxdapiserver/functions.php | 34 ++ whmcs/lxdapiserver/lib/lxd_api.php | 94 ++++ whmcs/lxdapiserver/lxdapiserver.php | 560 ++++++++++++++++++++++ whmcs/lxdapiserver/templates/overview.tpl | 68 +++ 4 files changed, 756 insertions(+) create mode 100644 whmcs/lxdapiserver/functions.php create mode 100644 whmcs/lxdapiserver/lib/lxd_api.php create mode 100644 whmcs/lxdapiserver/lxdapiserver.php create mode 100644 whmcs/lxdapiserver/templates/overview.tpl diff --git a/whmcs/lxdapiserver/functions.php b/whmcs/lxdapiserver/functions.php new file mode 100644 index 0000000..f13ec7a --- /dev/null +++ b/whmcs/lxdapiserver/functions.php @@ -0,0 +1,34 @@ +where('id', $serviceid) + ->update(['password' => encrypt($password)]); + return true; + } catch (Exception $e) { + lxdapiserver_log_error('save_password', $e->getMessage()); + return false; + } +} + +function lxdapiserver_log_error($function, $message) +{ + logActivity('[lxdapi Server] ' . $function . ': ' . $message); +} diff --git a/whmcs/lxdapiserver/lib/lxd_api.php b/whmcs/lxdapiserver/lib/lxd_api.php new file mode 100644 index 0000000..330f4aa --- /dev/null +++ b/whmcs/lxdapiserver/lib/lxd_api.php @@ -0,0 +1,94 @@ +serverip = $params['serverip']; + $this->serverport = $params['serverport'] ?: '8848'; + $this->apikey = $params['serveraccesshash']; + } + + public function get($endpoint, $data = []) + { + return $this->request('GET', $endpoint, $data); + } + + public function post($endpoint, $data = []) + { + return $this->request('POST', $endpoint, $data); + } + + public function delete($endpoint, $data = []) + { + return $this->request('DELETE', $endpoint, $data); + } + + private function request($method, $endpoint, $data = []) + { + $url = $this->protocol . '://' . $this->serverip . ':' . $this->serverport . $endpoint; + + if ($method === 'GET' && !empty($data) && strpos($endpoint, '?') === false) { + $url .= '?' . http_build_query($data); + } + + $ch = curl_init(); + + $headers = [ + 'X-API-Hash: ' . $this->apikey, + 'Content-Type: application/json', + ]; + + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, + ]); + + if (($method === 'POST' || $method === 'DELETE') && !empty($data)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + } + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + $curlErrno = curl_errno($ch); + curl_close($ch); + + if ($curlErrno) { + throw new Exception("连接失败: " . $curlError); + } + + $decoded = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception("响应解析失败: " . json_last_error_msg()); + } + + if ($httpCode >= 200 && $httpCode < 300) { + return [ + 'success' => isset($decoded['code']) ? $decoded['code'] == 200 : true, + 'message' => $decoded['msg'] ?? $decoded['message'] ?? '', + 'data' => $decoded['data'] ?? $decoded, + ]; + } + + return [ + 'success' => false, + 'message' => $decoded['msg'] ?? $decoded['message'] ?? 'HTTP ' . $httpCode, + 'data' => null, + ]; + } +} diff --git a/whmcs/lxdapiserver/lxdapiserver.php b/whmcs/lxdapiserver/lxdapiserver.php new file mode 100644 index 0000000..d770732 --- /dev/null +++ b/whmcs/lxdapiserver/lxdapiserver.php @@ -0,0 +1,560 @@ + 'WHMCS-LXD对接插件 by xkatld', + 'APIVersion' => 'v2.0.0-main', + 'RequiresServer' => true, + 'DefaultNonSSLPort' => '8848', + 'DefaultSSLPort' => '8848', + 'ServiceSingleSignOnLabel' => 'Login to Console', + 'AdminSingleSignOnLabel' => 'Login to Console as Admin', + ]; +} + +function lxdapiserver_TestConnection(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->get('/api/system/containers', []); + + if ($result['success']) { + return [ + 'success' => true, + 'error' => '', + ]; + } + + return [ + 'success' => false, + 'error' => $result['message'] ?: '连接失败', + ]; + + } catch (Exception $e) { + return [ + 'success' => false, + 'error' => $e->getMessage(), + ]; + } +} + +function lxdapiserver_ConfigOptions() +{ + return [ + 'cpus' => [ + 'FriendlyName' => 'CPU核心数', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '1', + 'Description' => 'CPU核心数量', + ], + 'memory' => [ + 'FriendlyName' => '内存 (MB)', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '512', + 'Description' => '内存大小,单位:MB', + ], + 'disk' => [ + 'FriendlyName' => '硬盘 (MB)', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '1024', + 'Description' => '硬盘大小,单位:MB', + ], + 'image' => [ + 'FriendlyName' => '系统镜像', + 'Type' => 'text', + 'Size' => '25', + 'Default' => 'alpine320', + 'Description' => '系统镜像名称', + ], + 'ingress' => [ + 'FriendlyName' => '入站带宽 (Mbit)', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '100', + 'Description' => '下载速度限制,单位:Mbit', + ], + 'egress' => [ + 'FriendlyName' => '出站带宽 (Mbit)', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '100', + 'Description' => '上传速度限制,单位:Mbit', + ], + 'traffic_limit' => [ + 'FriendlyName' => '月流量限制 (GB)', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '100', + 'Description' => '单位:GB', + ], + 'ipv4_pool_limit' => [ + 'FriendlyName' => 'IPv4地址池限制', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '0', + 'Description' => 'IPv4独立地址数量上限', + ], + 'ipv4_mapping_limit' => [ + 'FriendlyName' => 'IPv4端口映射限制', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '0', + 'Description' => 'IPv4端口转发规则上限', + ], + 'ipv6_pool_limit' => [ + 'FriendlyName' => 'IPv6地址池限制', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '0', + 'Description' => 'IPv6独立地址数量上限', + ], + 'ipv6_mapping_limit' => [ + 'FriendlyName' => 'IPv6端口映射限制', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '0', + 'Description' => 'IPv6端口转发规则上限', + ], + 'reverse_proxy_limit' => [ + 'FriendlyName' => '反向代理限制', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '0', + 'Description' => '反向代理域名数量上限', + ], + 'cpu_allowance' => [ + 'FriendlyName' => 'CPU使用率限制 (%)', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '50', + 'Description' => 'CPU占用百分比,单位:%', + ], + 'io_read' => [ + 'FriendlyName' => '磁盘读取限制 (MB/s)', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '100', + 'Description' => '单位:MB/s', + ], + 'io_write' => [ + 'FriendlyName' => '磁盘写入限制 (MB/s)', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '50', + 'Description' => '单位:MB/s', + ], + 'processes_limit' => [ + 'FriendlyName' => '最大进程数', + 'Type' => 'text', + 'Size' => '10', + 'Default' => '512', + 'Description' => '进程数量上限', + ], + 'allow_nesting' => [ + 'FriendlyName' => '嵌套虚拟化', + 'Type' => 'dropdown', + 'Options' => 'true,false', + 'Default' => 'true', + 'Description' => '支持Docker等虚拟化', + ], + 'memory_swap' => [ + 'FriendlyName' => 'Swap开关', + 'Type' => 'dropdown', + 'Options' => 'true,false', + 'Default' => 'true', + 'Description' => '虚拟内存开关', + ], + 'privileged' => [ + 'FriendlyName' => '特权模式', + 'Type' => 'dropdown', + 'Options' => 'true,false', + 'Default' => 'false', + 'Description' => '特权容器开关', + ], + ]; +} + +function lxdapiserver_CreateAccount(array $params) +{ + try { + $api = new LXD_API($params); + + $password = lxdapiserver_generate_password(); + + $containerName = 'lxd11451' . $params['serviceid']; + + $cpu = !empty($params['configoptions']['cpus']) ? $params['configoptions']['cpus'] : $params['configoption1']; + $memory = !empty($params['configoptions']['memory']) ? $params['configoptions']['memory'] : $params['configoption2']; + $disk = !empty($params['configoptions']['disk']) ? $params['configoptions']['disk'] : $params['configoption3']; + $image = !empty($params['configoptions']['image']) ? $params['configoptions']['image'] : $params['configoption4']; + $ingress = !empty($params['configoptions']['ingress']) ? $params['configoptions']['ingress'] : $params['configoption5']; + $egress = !empty($params['configoptions']['egress']) ? $params['configoptions']['egress'] : $params['configoption6']; + $traffic_limit = !empty($params['configoptions']['traffic_limit']) ? $params['configoptions']['traffic_limit'] : $params['configoption7']; + $ipv4_pool_limit = !empty($params['configoptions']['ipv4_pool_limit']) ? $params['configoptions']['ipv4_pool_limit'] : $params['configoption8']; + $ipv4_mapping_limit = !empty($params['configoptions']['ipv4_mapping_limit']) ? $params['configoptions']['ipv4_mapping_limit'] : $params['configoption9']; + $ipv6_pool_limit = !empty($params['configoptions']['ipv6_pool_limit']) ? $params['configoptions']['ipv6_pool_limit'] : $params['configoption10']; + $ipv6_mapping_limit = !empty($params['configoptions']['ipv6_mapping_limit']) ? $params['configoptions']['ipv6_mapping_limit'] : $params['configoption11']; + $reverse_proxy_limit = !empty($params['configoptions']['reverse_proxy_limit']) ? $params['configoptions']['reverse_proxy_limit'] : $params['configoption12']; + $cpu_allowance = !empty($params['configoptions']['cpu_allowance']) ? $params['configoptions']['cpu_allowance'] : $params['configoption13']; + $io_read = !empty($params['configoptions']['io_read']) ? $params['configoptions']['io_read'] : $params['configoption14']; + $io_write = !empty($params['configoptions']['io_write']) ? $params['configoptions']['io_write'] : $params['configoption15']; + $processes_limit = !empty($params['configoptions']['processes_limit']) ? $params['configoptions']['processes_limit'] : $params['configoption16']; + $allow_nesting = !empty($params['configoptions']['allow_nesting']) ? $params['configoptions']['allow_nesting'] === 'true' : $params['configoption17'] === 'true'; + $memory_swap = !empty($params['configoptions']['memory_swap']) ? $params['configoptions']['memory_swap'] === 'true' : $params['configoption18'] === 'true'; + $privileged = !empty($params['configoptions']['privileged']) ? $params['configoptions']['privileged'] === 'true' : $params['configoption19'] === 'true'; + + $requestData = [ + 'name' => $containerName, + 'image' => $image, + 'username' => 'user_' . $params['clientsdetails']['userid'], + 'password' => $password, + 'cpu' => (int)$cpu, + 'memory' => (int)$memory, + 'disk' => (int)$disk, + 'ingress' => (int)$ingress, + 'egress' => (int)$egress, + 'traffic_limit' => (int)$traffic_limit, + 'ipv4_pool_limit' => (int)$ipv4_pool_limit, + 'ipv4_mapping_limit' => (int)$ipv4_mapping_limit, + 'ipv6_pool_limit' => (int)$ipv6_pool_limit, + 'ipv6_mapping_limit' => (int)$ipv6_mapping_limit, + 'reverse_proxy_limit' => (int)$reverse_proxy_limit, + 'cpu_allowance' => (int)$cpu_allowance, + 'io_read' => (int)$io_read, + 'io_write' => (int)$io_write, + 'processes_limit' => (int)$processes_limit, + 'allow_nesting' => $allow_nesting, + 'memory_swap' => $memory_swap, + 'privileged' => $privileged, + ]; + + $result = $api->post('/api/system/containers', $requestData); + + if (!$result['success']) { + return $result['message'] ?? '创建容器失败'; + } + + lxdapiserver_save_password($params['serviceid'], $password); + + $updateData = [ + 'domain' => $containerName, + ]; + + if (!empty($result['data']['ipv4'])) { + $updateData['dedicatedip'] = $result['data']['ipv4']; + } + + Capsule::table('tblhosting') + ->where('id', $params['serviceid']) + ->update($updateData); + + return 'success'; + + } catch (Exception $e) { + lxdapiserver_log_error('CreateAccount', $e->getMessage()); + return $e->getMessage(); + } +} + +function lxdapiserver_SuspendAccount(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->post('/api/system/containers/pause?name=' . urlencode($params['domain']), []); + + return $result['success'] ? 'success' : ($result['message'] ?? '暂停失败'); + + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_UnsuspendAccount(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->post('/api/system/containers/resume?name=' . urlencode($params['domain']), []); + + return $result['success'] ? 'success' : ($result['message'] ?? '恢复失败'); + + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_TerminateAccount(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->delete('/api/system/containers?name=' . urlencode($params['domain'])); + + return $result['success'] ? 'success' : ($result['message'] ?? '删除失败'); + + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_ChangePassword(array $params) +{ + try { + $api = new LXD_API($params); + + $result = $api->post('/api/system/containers/reset-password', [ + 'name' => $params['domain'], + 'password' => $params['password'] + ]); + + if ($result['success']) { + lxdapiserver_save_password($params['serviceid'], $params['password']); + return 'success'; + } + + return $result['message'] ?? '修改密码失败'; + + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_ClientArea(array $params) +{ + $page = $_GET['page'] ?? ''; + + if ($page !== 'manage') { + return []; + } + + try { + $api = new LXD_API($params); + + $result = $api->post('/api/system/containers/access-code', [ + 'container_name' => $params['domain'] + ]); + + $jumpUrl = ''; + $iframeUrl = ''; + $errorMsg = ''; + + if ($result['success'] && !empty($result['data']['access_code'])) { + $accessCode = $result['data']['access_code']; + $protocol = 'https'; + $baseUrl = $protocol . '://' . $params['serverip'] . ':' . $params['serverport']; + $jumpUrl = $baseUrl . '/container/dashboard?hash=' . $accessCode; + $iframeUrl = $baseUrl . '/container/dashboard/lite?hash=' . $accessCode; + } else { + $errorMsg = $result['message'] ?? '获取访问码失败'; + } + + return [ + 'tabOverviewReplacementTemplate' => 'templates/overview.tpl', + 'templateVariables' => [ + 'jump_url' => $jumpUrl, + 'iframe_url' => $iframeUrl, + 'error_msg' => $errorMsg, + ], + ]; + + } catch (Exception $e) { + return [ + 'tabOverviewReplacementTemplate' => 'templates/overview.tpl', + 'templateVariables' => [ + 'error_msg' => $e->getMessage(), + ], + ]; + } +} + +function lxdapiserver_AdminCustomButtonArray() +{ + return [ + '开机' => 'start', + '关机' => 'stop', + '重启' => 'reboot', + '重装系统' => 'reinstall', + '同步状态' => 'sync', + '重置流量' => 'traffic_reset', + ]; +} + +function lxdapiserver_start(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->post('/api/system/containers/start?name=' . urlencode($params['domain']), []); + return $result['success'] ? 'success' : ($result['message'] ?? '开机失败'); + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_stop(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->post('/api/system/containers/stop?name=' . urlencode($params['domain']), []); + return $result['success'] ? 'success' : ($result['message'] ?? '关机失败'); + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_reboot(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->post('/api/system/containers/restart?name=' . urlencode($params['domain']), []); + return $result['success'] ? 'success' : ($result['message'] ?? '重启失败'); + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_reinstall(array $params) +{ + try { + $api = new LXD_API($params); + $password = lxdapiserver_generate_password(); + + $result = $api->post('/api/system/containers/reinstall?name=' . urlencode($params['domain']), [ + 'image' => $params['configoption4'], + 'password' => $password + ]); + + if ($result['success']) { + lxdapiserver_save_password($params['serviceid'], $password); + return 'success'; + } + + return $result['message'] ?? '重装失败'; + + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_sync(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->get('/api/system/containers/status?name=' . urlencode($params['domain'])); + + if ($result['success'] && !empty($result['data'])) { + $updateData = []; + + if (!empty($result['data']['ipv4'])) { + $updateData['dedicatedip'] = $result['data']['ipv4']; + } + + if (isset($result['data']['status'])) { + $status = strtoupper($result['data']['status']); + if ($status === 'RUNNING') { + $updateData['domainstatus'] = 'Active'; + } elseif ($status === 'STOPPED') { + $updateData['domainstatus'] = 'Suspended'; + } + } + + if (!empty($updateData)) { + Capsule::table('tblhosting') + ->where('id', $params['serviceid']) + ->update($updateData); + } + + return 'success'; + } + + return $result['message'] ?? '同步失败'; + + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_traffic_reset(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->post('/api/system/traffic/reset?name=' . urlencode($params['domain']), []); + return $result['success'] ? 'success' : ($result['message'] ?? '重置流量失败'); + } catch (Exception $e) { + return $e->getMessage(); + } +} + +function lxdapiserver_AdminServicesTabFields(array $params) +{ + try { + $api = new LXD_API($params); + $result = $api->get('/api/system/containers/status?name=' . urlencode($params['domain'])); + + if ($result['success'] && !empty($result['data'])) { + $data = $result['data']; + return [ + '容器状态' => $data['status'] ?? 'Unknown', + 'IPv4地址' => $data['ipv4'] ?? 'N/A', + 'IPv6地址' => $data['ipv6'] ?? 'N/A', + 'CPU使用率' => isset($data['cpu_percent']) ? number_format($data['cpu_percent'], 2) . '%' : 'N/A', + '内存使用' => $data['memory_usage'] ?? 'N/A', + '硬盘使用' => $data['disk_usage'] ?? 'N/A', + '流量使用' => $data['traffic_usage'] ?? 'N/A', + ]; + } + } catch (Exception $e) { + lxdapiserver_log_error('AdminServicesTabFields', $e->getMessage()); + } + + return []; +} + +function lxdapiserver_ServiceSingleSignOn(array $params) +{ + try { + $api = new LXD_API($params); + + $result = $api->post('/api/system/console/create-token', [ + 'hostname' => $params['domain'], + ]); + + if ($result['success'] && !empty($result['data']['token'])) { + $consoleUrl = 'https://' . $params['serverip'] . ':' . $params['serverport'] . '/console?token=' . $result['data']['token']; + + return [ + 'success' => true, + 'redirectTo' => $consoleUrl, + ]; + } + + return [ + 'success' => false, + 'errorMsg' => $result['message'] ?? '创建控制台会话失败', + ]; + + } catch (Exception $e) { + return [ + 'success' => false, + 'errorMsg' => $e->getMessage(), + ]; + } +} diff --git a/whmcs/lxdapiserver/templates/overview.tpl b/whmcs/lxdapiserver/templates/overview.tpl new file mode 100644 index 0000000..baf60bc --- /dev/null +++ b/whmcs/lxdapiserver/templates/overview.tpl @@ -0,0 +1,68 @@ +