diff --git a/Mofang/clicd.php b/Mofang/clicd.php index 7313aa5..d7c313a 100644 --- a/Mofang/clicd.php +++ b/Mofang/clicd.php @@ -37,10 +37,10 @@ function clicd_json_response($payload) function clicd_MetaData() { return [ - 'DisplayName' => 'CLICD 对接模块 by 欢-Huan and ChatGPT 5.5', + 'DisplayName' => 'CLICD 对接模块 by 欢-Huan and ChatGPT 5.5 and DeepSeek V4', 'APIVersion' => '1.1', 'HelpDoc' => 'https://github.com/MengMengCode/CLICD', - 'version' => '1.0.1', + 'version' => '1.0.5', ]; } @@ -59,10 +59,19 @@ function clicd_ConfigOptions() ['type' => 'text', 'name' => '入站流量 GB', 'description' => 'in_out 模式下入站流量限制,0 表示不限制', 'default' => '0', 'key' => 'traffic_in_gb'], ['type' => 'text', 'name' => '出站流量 GB', 'description' => 'in_out 模式下出站流量限制,0 表示不限制', 'default' => '0', 'key' => 'traffic_out_gb'], ['type' => 'text', 'name' => 'IO 速度 MB/s', 'description' => '磁盘 IO 限制,0 表示不限制', 'default' => '0', 'key' => 'io_speed_mbps'], + ['type' => 'dropdown', 'name' => '分配 NAT', 'description' => '开通时是否分配 NAT 端口映射', 'default' => 'true', 'key' => 'assign_nat', 'options' => ['true' => '启用', 'false' => '禁用']], ['type' => 'text', 'name' => 'NAT 端口数量', 'description' => '开通时分配的端口映射数量,最小 2', 'default' => '2', 'key' => 'port_mapping_count'], ['type' => 'text', 'name' => '快照配额', 'description' => '每台实例允许保留的快照数量', 'default' => '3', 'key' => 'snapshot_limit'], ['type' => 'text', 'name' => '额外端口', 'description' => '逗号分隔的容器端口,例如 80,443', 'default' => '', 'key' => 'extra_ports'], + ['type' => 'dropdown', 'name' => '自动公网 IPv4', 'description' => '开通时是否从 CLICD 公网 IPv4 池分配独立 IPv4', 'default' => 'false', 'key' => 'assign_ipv4', 'options' => ['true' => '启用', 'false' => '禁用']], + ['type' => 'text', 'name' => '公网 IPv4 数量', 'description' => '自动分配公网 IPv4 的数量,通常填写 1', 'default' => '1', 'key' => 'ipv4_count'], + ['type' => 'text', 'name' => '指定公网 IPv4', 'description' => '指定分配的公网 IPv4,多个用逗号分隔;留空则从地址池自动分配', 'default' => '', 'key' => 'public_ipv4s'], ['type' => 'dropdown', 'name' => '自动 IPv6', 'description' => '开通时自动分配 IPv6', 'default' => 'false', 'key' => 'assign_ipv6', 'options' => ['true' => '启用', 'false' => '禁用']], + ['type' => 'text', 'name' => 'IPv6 数量', 'description' => '自动分配 IPv6 的数量,通常填写 1', 'default' => '1', 'key' => 'ipv6_count'], + ['type' => 'text', 'name' => '指定 IPv6', 'description' => '指定分配的 IPv6 地址,多个用逗号分隔;留空则从地址池自动分配', 'default' => '', 'key' => 'ipv6_addresses'], + ['type' => 'dropdown', 'name' => 'SSH 鉴权模式', 'description' => 'auto_password=自动生成密码,password=使用指定密码,key=使用 SSH 公钥', 'default' => 'auto_password', 'key' => 'ssh_auth_mode', 'options' => ['auto_password' => '自动密码', 'password' => '指定密码', 'key' => 'SSH 公钥']], + ['type' => 'text', 'name' => '指定 SSH 密码', 'description' => 'SSH 鉴权模式为 password 时使用;其他模式留空', 'default' => '', 'key' => 'ssh_password'], + ['type' => 'text', 'name' => 'SSH 公钥', 'description' => 'SSH 鉴权模式为 key 时使用;填写完整 public key', 'default' => '', 'key' => 'ssh_public_key'], ['type' => 'dropdown', 'name' => '同步到期时间', 'description' => '开通/续费时把魔方到期日期同步到 CLICD,格式会转换为 YYYY-MM-DD', 'default' => 'true', 'key' => 'sync_expiry', 'options' => ['true' => '启用', 'false' => '禁用']], ]; } @@ -203,16 +212,101 @@ function clicd_container_name($params) return trim($name, '-.'); } -function clicd_public_host($params, $container = []) +function clicd_host_id($params) +{ + foreach (['hostid', 'id', 'serviceid', 'service_id', 'relid'] as $key) { + if (!empty($params[$key]) && is_numeric($params[$key])) { + return (int)$params[$key]; + } + } + return 0; +} +function clicd_first_string($value) +{ + if (is_array($value)) { + foreach ($value as $item) { + if (is_array($item)) { + foreach (['address', 'ip', 'ipv4', 'public_ip', 'public_ipv4'] as $key) { + if (!empty($item[$key])) { + $itemValue = trim((string)$item[$key]); + if ($itemValue !== '') { + return $itemValue; + } + } + } + continue; + } + + $itemValue = trim((string)$item); + if ($itemValue !== '') { + return $itemValue; + } + } + return ''; + } + + $value = trim((string)$value); + return $value; +} + +function clicd_public_host_from_container($container = []) { if (is_array($container)) { - foreach (['nat_public_ip', 'public_ip', 'host_ip', 'external_ip', 'node_ip', 'nat_host'] as $key) { + foreach (['public_ipv4s', 'public_ipv4', 'public_ip', 'ipv4_addresses', 'ipv4', 'nat_public_ip', 'host_ip', 'external_ip', 'node_ip', 'nat_host'] as $key) { if (!empty($container[$key])) { - return trim((string)$container[$key]); + $value = clicd_first_string($container[$key]); + if ($value !== '') { + return $value; + } } } } + return ''; +} + +function clicd_public_ipv4_from_routing($params, $container = []) +{ + if (!is_array($container)) { + return ''; + } + + $containerId = isset($container['id']) ? (string)$container['id'] : ''; + $containerName = isset($container['name']) ? (string)$container['name'] : clicd_container_name($params); + + $res = clicd_request($params, '/api/v1/routing', [], 'GET', 30); + if (!clicd_success($res) || empty($res['data']['ipv4_assignments']) || !is_array($res['data']['ipv4_assignments'])) { + return ''; + } + + foreach ($res['data']['ipv4_assignments'] as $assignment) { + if (!is_array($assignment)) { + continue; + } + $matchId = $containerId !== '' && isset($assignment['container_id']) && (string)$assignment['container_id'] === $containerId; + $matchName = $containerName !== '' && isset($assignment['container_name']) && (string)$assignment['container_name'] === $containerName; + if ($matchId || $matchName) { + return clicd_first_string($assignment['address'] ?? ''); + } + } + + return ''; +} + +function clicd_public_host($params, $container = [], $useRouting = false) +{ + $fromContainer = clicd_public_host_from_container($container); + if ($fromContainer !== '') { + return $fromContainer; + } + + if ($useRouting) { + $fromRouting = clicd_public_ipv4_from_routing($params, $container); + if ($fromRouting !== '') { + return $fromRouting; + } + } + foreach (['server_ip', 'ip'] as $key) { if (!empty($params[$key])) { $value = trim((string)$params[$key]); @@ -403,6 +497,24 @@ function clicd_extra_ports($value) return array_values(array_unique($ports)); } +function clicd_csv_values($value) +{ + if (is_array($value)) { + $parts = $value; + } else { + $parts = preg_split('/[,;\s]+/', (string)$value); + } + + $result = []; + foreach ($parts as $part) { + $part = trim((string)$part); + if ($part !== '') { + $result[] = $part; + } + } + return array_values(array_unique($result)); +} + function clicd_expiry_from_params($params) { $options = $params['configoptions'] ?? []; @@ -435,6 +547,21 @@ function clicd_container_payload($params) { $options = $params['configoptions'] ?? []; $trafficMode = $options['traffic_mode'] ?? 'total'; + $assignNat = clicd_bool_option($options['assign_nat'] ?? 'true', true); + $assignIpv4 = clicd_bool_option($options['assign_ipv4'] ?? 'false', false); + $assignIpv6 = clicd_bool_option($options['assign_ipv6'] ?? 'false', false); + $publicIpv4s = clicd_csv_values($options['public_ipv4s'] ?? ''); + $ipv6Addresses = clicd_csv_values($options['ipv6_addresses'] ?? ''); + if (!empty($publicIpv4s)) { + $assignIpv4 = true; + } + if (!empty($ipv6Addresses)) { + $assignIpv6 = true; + } + $sshAuthMode = strtolower(trim((string)($options['ssh_auth_mode'] ?? 'auto_password'))); + if (!in_array($sshAuthMode, ['auto_password', 'password', 'key'], true)) { + $sshAuthMode = 'auto_password'; + } return [ 'name' => clicd_container_name($params), @@ -451,9 +578,18 @@ function clicd_container_payload($params) 'traffic_out_gb' => clicd_int_option($options, 'traffic_out_gb', 0), 'io_speed_mbps' => clicd_int_option($options, 'io_speed_mbps', 0), 'extra_ports' => clicd_extra_ports($options['extra_ports'] ?? ''), - 'port_mapping_count' => max(2, clicd_int_option($options, 'port_mapping_count', 2)), + 'port_mapping_count' => $assignNat ? max(2, clicd_int_option($options, 'port_mapping_count', 2)) : 0, + 'assign_nat' => $assignNat, + 'assign_ipv4' => $assignIpv4, + 'ipv4_count' => max(1, clicd_int_option($options, 'ipv4_count', 1)), + 'public_ipv4s' => $publicIpv4s, 'snapshot_limit' => max(1, clicd_int_option($options, 'snapshot_limit', 3)), - 'assign_ipv6' => clicd_bool_option($options['assign_ipv6'] ?? 'false', false), + 'assign_ipv6' => $assignIpv6, + 'ipv6_count' => max(1, clicd_int_option($options, 'ipv6_count', 1)), + 'ipv6_addresses' => $ipv6Addresses, + 'ssh_auth_mode' => $sshAuthMode, + 'ssh_password' => (string)($options['ssh_password'] ?? ''), + 'ssh_public_key' => trim((string)($options['ssh_public_key'] ?? '')), 'expires_at' => clicd_expiry_from_params($params), ]; } @@ -477,6 +613,9 @@ function clicd_request_value($key, $default = '') { if (function_exists('input')) { $value = input('param.' . $key); + if ($value === null) { + $value = input('*.' . $key); + } return $value === null ? $default : $value; } if (isset($_POST[$key])) { @@ -821,16 +960,35 @@ function clicd_info_ajax($params) ]; } +function clicd_domain_status_from_container($container) +{ + if (!is_array($container)) { + return 'Active'; + } + + if (!empty($container['policy_blocked'])) { + return 'Suspended'; + } + + $status = strtolower(trim((string)($container['status'] ?? ''))); + if (in_array($status, ['suspended', 'blocked', 'policy_blocked', 'disabled'], true)) { + return 'Suspended'; + } + + return 'Active'; +} + function clicd_update_host_from_container($params, $container) { - if (empty($params['hostid']) || !is_array($container)) { + $hostId = clicd_host_id($params); + if ($hostId <= 0 || !is_array($container)) { return; } $update = [ - 'domainstatus' => (($container['status'] ?? '') === 'running') ? 'Active' : 'Suspended', + 'domainstatus' => clicd_domain_status_from_container($container), 'username' => 'root', - 'dedicatedip' => clicd_public_host($params, $container), + 'dedicatedip' => clicd_public_host($params, $container, true), ]; $sshPort = clicd_container_ssh_port($container); @@ -844,7 +1002,7 @@ function clicd_update_host_from_container($params, $container) } try { - Db::name('host')->where('id', $params['hostid'])->update($update); + Db::name('host')->where('id', $hostId)->update($update); } catch (\Exception $e) { clicd_debug('host update failed', $e->getMessage()); } @@ -879,21 +1037,24 @@ function clicd_CreateAccount($params) return ['status' => 'error', 'msg' => clicd_message($res, '开通失败')]; } - $detail = clicd_find_container($params); - if (clicd_success($detail) && isset($detail['data'])) { - clicd_update_host_from_container($params, $detail['data']); - } elseif (!empty($params['hostid'])) { + $hostId = clicd_host_id($params); + if ($hostId > 0) { try { - Db::name('host')->where('id', $params['hostid'])->update([ + Db::name('host')->where('id', $hostId)->update([ 'domainstatus' => 'Active', 'username' => 'root', - 'dedicatedip' => clicd_public_host($params), + 'dedicatedip' => clicd_public_ipv4_from_routing($params) ?: clicd_public_host($params), ]); } catch (\Exception $e) { return ['status' => 'error', 'msg' => '开通成功但同步魔方数据库失败: ' . $e->getMessage()]; } } + $detail = clicd_find_container($params); + if (clicd_success($detail) && isset($detail['data'])) { + clicd_update_host_from_container($params, $detail['data']); + } + return ['status' => 'success', 'msg' => clicd_message($res, '开通成功')]; } @@ -1001,9 +1162,10 @@ function clicd_CrackPassword($params, $new_pass) } $password = $res['data']['ssh_password'] ?? $res['data']['password'] ?? $new_pass; - if (!empty($params['hostid'])) { + $hostId = clicd_host_id($params); + if ($hostId > 0) { try { - Db::name('host')->where('id', $params['hostid'])->update(['password' => clicd_store_password($password)]); + Db::name('host')->where('id', $hostId)->update(['password' => clicd_store_password($password)]); $detail = clicd_find_container($params); if (clicd_success($detail) && isset($detail['data'])) { clicd_update_host_from_container($params, $detail['data']); @@ -1262,19 +1424,155 @@ function clicd_webssh($params) ]; } +function clicd_firewallList($params) +{ + $container = []; + $containerId = clicd_container_api_id($params, $container); + $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', [], 'GET', 30); + if (!clicd_success($res) || empty($res['data'])) { + return ['status' => 'error', 'msg' => clicd_message($res, '获取防火墙设置失败')]; + } + + return [ + 'status' => 200, + 'msg' => '获取成功', + 'data' => $res['data'], + ]; +} + +function clicd_firewallUpdate($params) +{ + $input = clicd_json_input(); + $enabled = clicd_param_value($input, 'enabled', 'true'); + $enabled = filter_var($enabled, FILTER_VALIDATE_BOOLEAN); + $rules = clicd_param_value($input, 'rules', '[]'); + + if (is_string($rules)) { + $decodedRules = json_decode($rules, true); + if (is_array($decodedRules)) { + $rules = $decodedRules; + } + } + if (!is_array($rules)) { + $rules = []; + } + + $payload = [ + 'enabled' => $enabled, + 'rules' => $rules, + ]; + + $container = []; + $containerId = clicd_container_api_id($params, $container); + $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', $payload, 'PUT', 30); + if (!clicd_success($res)) { + return ['status' => 'error', 'msg' => clicd_message($res, '更新防火墙设置失败')]; + } + + // GET after PUT to confirm the actual state after CLICD processes it + $getRes = clicd_request($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', [], 'GET', 30); + $actualData = []; + if (clicd_success($getRes) && !empty($getRes['data']) && is_array($getRes['data'])) { + $actualData = $getRes['data']; + } + + return [ + 'status' => 200, + 'msg' => clicd_message($res, '防火墙设置已更新'), + 'data' => $actualData, + ]; +} + +function clicd_firewall_ajax($params) +{ + $input = clicd_json_input(); + $action = strtolower(trim((string)clicd_param_value($input, 'action', ''))); + $debug = [clicd_debug_entry('Firewall ajax received', [ + 'action' => $action, + 'input' => $input, + 'query' => $_GET, + ])]; + + $container = []; + $containerId = clicd_container_api_id($params, $container); + $debug[] = clicd_debug_entry('Container resolved', [ + 'container_id' => $containerId, + 'container' => [ + 'id' => $container['id'] ?? null, + 'uuid' => $container['uuid'] ?? null, + 'name' => $container['name'] ?? null, + ], + ]); + + if (!in_array($action, ['list', 'update'], true)) { + return ['status' => 'error', 'msg' => '未知防火墙操作', 'debug' => $debug]; + } + + if ($action === 'list') { + $call = clicd_request_debug($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', [], 'GET', 30); + $debug[] = $call['debug']; + $res = $call['response']; + if (!clicd_success($res) || empty($res['data'])) { + return ['status' => 'error', 'msg' => clicd_message($res, '获取防火墙设置失败'), 'debug' => $debug]; + } + return [ + 'status' => 'success', + 'msg' => '获取成功', + 'data' => $res['data'], + 'debug' => $debug, + ]; + } + + // update + $enabled = clicd_param_value($input, 'enabled', 'true'); + $enabled = filter_var($enabled, FILTER_VALIDATE_BOOLEAN); + $rules = clicd_param_value($input, 'rules', '[]'); + + if (is_string($rules)) { + $decodedRules = json_decode($rules, true); + if (is_array($decodedRules)) { + $rules = $decodedRules; + } + } + if (!is_array($rules)) { + $rules = []; + } + + $payload = [ + 'enabled' => $enabled, + 'rules' => $rules, + ]; + + $call = clicd_request_debug($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', $payload, 'PUT', 30); + $debug[] = $call['debug']; + $res = $call['response']; + + if (!clicd_success($res)) { + return ['status' => 'error', 'msg' => clicd_message($res, '更新防火墙设置失败'), 'debug' => $debug]; + } + + return [ + 'status' => 'success', + 'msg' => clicd_message($res, '防火墙设置已更新'), + 'data' => $res['data'] ?? [], + 'debug' => $debug, + ]; +} + function clicd_AllowFunction() { return [ - 'client' => ['TrafficReset', 'randomPort', 'addNat', 'updateNat', 'deleteNat', 'natList', 'infoData', 'webssh'], - 'admin' => ['TrafficReset', 'randomPort', 'addNat', 'updateNat', 'deleteNat', 'natList', 'infoData', 'webssh'], + 'client' => ['TrafficReset', 'randomPort', 'addNat', 'updateNat', 'deleteNat', 'natList', 'infoData', 'webssh', 'firewallList', 'firewallUpdate'], + 'admin' => ['TrafficReset', 'randomPort', 'addNat', 'updateNat', 'deleteNat', 'natList', 'infoData', 'webssh', 'firewallList', 'firewallUpdate'], ]; } function clicd_ClientArea($params) { return [ - 'info' => ['name' => '实例信息'], - 'nat' => ['name' => 'NAT转发'], + 'info' => ['name' => '实例信息'], + 'nat' => ['name' => 'NAT转发'], + 'firewall' => ['name' => '防火墙'], ]; } @@ -1287,8 +1585,11 @@ function clicd_ClientAreaOutput($params, $key) if ($func === 'infoajax') { clicd_json_response(clicd_info_ajax($params)); } + if ($func === 'firewallajax') { + clicd_json_response(clicd_firewall_ajax($params)); + } - if (!in_array($key, ['info', 'nat'], true)) { + if (!in_array($key, ['info', 'nat', 'firewall'], true)) { return ''; } @@ -1298,6 +1599,7 @@ function clicd_ClientAreaOutput($params, $key) } $c = $res['data']; + $publicHost = clicd_public_host($params, $c, true); if ($key === 'nat') { $operation = clicd_handle_nat_post($params); @@ -1315,8 +1617,8 @@ function clicd_ClientAreaOutput($params, $key) 'container' => $c, 'container_name'=> $c['name'] ?? clicd_container_name($params), 'ssh_port' => $c['ssh_port'] ?? '', - 'server_ip' => $params['server_ip'] ?? parse_url(clicd_base_url($params), PHP_URL_HOST), - 'nat_host' => $params['server_ip'] ?? parse_url(clicd_base_url($params), PHP_URL_HOST), + 'server_ip' => $publicHost, + 'nat_host' => $publicHost, 'operation_msg' => $operationMsg, 'service_id' => clicd_request_value('id', $params['hostid'] ?? ''), 'area_key' => 'nat', @@ -1325,6 +1627,19 @@ function clicd_ClientAreaOutput($params, $key) ]; } + if ($key === 'firewall') { + return [ + 'template' => 'templates/firewall.html', + 'vars' => [ + 'container' => $c, + 'container_name' => $c['name'] ?? clicd_container_name($params), + 'server_ip' => $publicHost, + 'service_id' => clicd_request_value('id', $params['hostid'] ?? ''), + 'area_key' => 'firewall', + ], + ]; + } + $initialRxBytes = (int)($c['traffic_used_rx'] ?? $c['rx_bytes'] ?? 0); $initialTxBytes = (int)($c['traffic_used_tx'] ?? $c['tx_bytes'] ?? 0); $initialTrafficUsed = ($initialRxBytes || $initialTxBytes) ? round(($initialRxBytes + $initialTxBytes) / 1073741824, 2) : '-'; @@ -1342,8 +1657,8 @@ function clicd_ClientAreaOutput($params, $key) 'vars' => [ 'container' => $c, 'status_text' => (($c['status'] ?? '') === 'running') ? '运行中' : '已关机', - 'server_ip' => $params['server_ip'] ?? parse_url(clicd_base_url($params), PHP_URL_HOST), - 'ssh_host' => $params['server_ip'] ?? parse_url(clicd_base_url($params), PHP_URL_HOST), + 'server_ip' => $publicHost, + 'ssh_host' => $publicHost, 'ssh_port' => $c['ssh_port'] ?? '', 'ssh_password' => $c['ssh_password'] ?? '', 'ipv4' => $c['ip'] ?? '', @@ -1366,3 +1681,7 @@ function clicd_ClientAreaOutput($params, $key) ], ]; } + + + + diff --git a/Mofang/clicd.zip b/Mofang/clicd.zip deleted file mode 100644 index 66426cc..0000000 Binary files a/Mofang/clicd.zip and /dev/null differ diff --git a/Mofang/handlers/webssh.php b/Mofang/handlers/webssh.php index 887308a..3be35c7 100644 --- a/Mofang/handlers/webssh.php +++ b/Mofang/handlers/webssh.php @@ -1,4 +1,4 @@ - +.clicd-fw-panel{font-size:14px;color:#1f2937} +.clicd-fw-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:16px} +.clicd-fw-card{border:1px solid #e5e7eb;border-radius:6px;padding:12px;background:#fff} +.clicd-fw-label{color:#6b7280;font-size:12px;margin-bottom:4px} +.clicd-fw-value{font-size:18px;font-weight:600;word-break:break-all} +.clicd-fw-section{border:1px solid #e5e7eb;border-radius:6px;background:#fff;padding:12px;margin-top:8px} +.clicd-fw-title{font-weight:600;margin:18px 0 8px} +.clicd-fw-muted{color:#6b7280} +.clicd-fw-toggle-row{display:flex;align-items:center;gap:12px;margin-bottom:12px} +.clicd-fw-toggle{position:relative;display:inline-flex;width:48px;height:26px;cursor:pointer} +.clicd-fw-toggle input{opacity:0;width:0;height:0} +.clicd-fw-toggle-slider{position:absolute;inset:0;background:#d1d5db;border-radius:26px;transition:.25s} +.clicd-fw-toggle-slider:before{content:"";position:absolute;width:22px;height:22px;border-radius:50%;background:#fff;top:2px;left:2px;transition:.25s}.clicd-fw-toggle .clicd-fw-toggle-slider:before{width:16px;height:16px;top:2px;left:2px} +.clicd-fw-toggle input:checked+.clicd-fw-toggle-slider{background:#10b981} +.clicd-fw-toggle input:checked+.clicd-fw-toggle-slider:before{transform:translateX(22px)} +.clicd-fw-toggle-label{font-size:14px;font-weight:500} +.clicd-fw-status{font-size:13px;color:#6b7280} +.clicd-fw-rule-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;align-items:end} +.clicd-fw-field label{display:block;color:#6b7280;font-size:12px;margin-bottom:4px} +.clicd-fw-input,.clicd-fw-select{width:100%;height:34px;border:1px solid #d1d5db;border-radius:4px;padding:6px 8px;box-sizing:border-box} +.clicd-fw-input:focus,.clicd-fw-select:focus{border-color:#2563eb;outline:none} +.clicd-fw-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:end} +.clicd-fw-btn{height:34px;border:1px solid #2563eb;background:#2563eb;color:#fff;border-radius:4px;padding:0 12px;cursor:pointer;font-size:13px} +.clicd-fw-btn[disabled]{opacity:.6;cursor:not-allowed} +.clicd-fw-btn-secondary{border-color:#d1d5db;background:#fff;color:#374151} +.clicd-fw-btn-danger{border-color:#dc2626;background:#dc2626;color:#fff} +.clicd-fw-btn-sm{height:30px;padding:0 10px;font-size:12px} +.clicd-fw-rules{display:flex;flex-direction:column;gap:10px;margin-top:8px} +.clicd-fw-rule{border:1px solid #e5e7eb;border-radius:6px;background:#fff;padding:12px} +.clicd-fw-rule-header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;flex-wrap:wrap} +.clicd-fw-rule-direction{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600} +.clicd-fw-direction-in{background:#dbeafe;color:#1d4ed8} +.clicd-fw-direction-out{background:#fef3c7;color:#92400e} +.clicd-fw-rule-action{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600} +.clicd-fw-action-ACCEPT{background:#d1fae5;color:#065f46} +.clicd-fw-action-DROP{background:#fee2e2;color:#991b1b} +.clicd-fw-rule-desc{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px} +.clicd-fw-rule-detail{font-size:13px;color:#374151;display:flex;align-items:center;gap:8px;flex-wrap:wrap} +.clicd-fw-rule-detail .sep{color:#d1d5db} +.clicd-fw-rule-edit-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid #e5e7eb} +.clicd-fw-message{border:1px solid #bfdbfe;background:#eff6ff;color:#1d4ed8;border-radius:6px;padding:10px 12px;margin-bottom:12px;display:none} +.clicd-fw-message.error{border-color:#fecaca;background:#fef2f2;color:#b91c1c} +.clicd-fw-debug{margin-top:12px;border:1px dashed #d1d5db;border-radius:6px;background:#f9fafb;padding:10px;color:#374151;white-space:pre-wrap;font-size:12px;display:none} +.clicd-fw-modal-mask{position:fixed;inset:0;background:rgba(15,23,42,.42);display:none;align-items:center;justify-content:center;z-index:9999;padding:16px} +.clicd-fw-modal{width:min(420px,100%);background:#fff;border-radius:6px;border:1px solid #e5e7eb;box-shadow:0 18px 48px rgba(15,23,42,.22);padding:16px} +.clicd-fw-modal-title{font-size:16px;font-weight:700;color:#111827;margin-bottom:8px} +.clicd-fw-modal-body{font-size:14px;color:#4b5563;line-height:1.6;margin-bottom:14px} +.clicd-fw-modal-actions{display:flex;justify-content:flex-end;gap:8px} + + +
+
+ +
+
+
实例名称
+
{$container_name}
+
+
+
IP 地址
+
{$server_ip}
+
+
+ +
+
+ + 启用防火墙 + 加载中... +
+
+ +
添加规则
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
防火墙规则
+
+
加载中...
+
+ +

+
+    
+
+
确认删除
+
确认删除该规则?
+
+ + +
+
+
+
+ + diff --git a/Mofang/templates/info.html b/Mofang/templates/info.html index d6d27b9..d3440ab 100644 --- a/Mofang/templates/info.html +++ b/Mofang/templates/info.html @@ -1,4 +1,4 @@ -