diff --git a/Mofang/README.md b/Mofang/README.md index 23012d1..902b873 100644 --- a/Mofang/README.md +++ b/Mofang/README.md @@ -10,6 +10,7 @@ README.md handlers/ webssh.php templates/ + firewall.html info.html nat.html ``` @@ -93,11 +94,12 @@ Content-Type: application/json ## 客户区页面 -模块提供两个客户区选项卡: +模块提供三个客户区选项卡: ```text 实例信息 NAT转发 +防火墙 ``` 客户区按钮提供: @@ -197,6 +199,65 @@ DELETE /api/v1/containers/{id}/port-mappings/{index} } ``` +## 防火墙 + +防火墙是独立客户区页面,支持: + +- 查看防火墙启用状态、默认动作和规则列表 +- 启用 / 停用防火墙 +- 设置默认动作:未匹配拒绝或未匹配放行 +- 添加规则 +- 编辑规则 +- 删除规则 +- 单独启用 / 停用某条规则 + +页面会先在前端修改规则列表和开关状态,点击“保存设置”后才统一同步到 CLICD。这样可以避免每次切换开关、修改默认动作或编辑规则时都立即请求后端,减少客户区卡顿。 + +注意:防火墙关闭时也可以保存规则;关闭只表示暂时不接管该容器流量,不代表规则必须清空。 + +使用的 CLICD API: + +```text +GET /api/v1/containers/{id}/firewall +PUT /api/v1/containers/{id}/firewall +``` + +更新防火墙时必须使用 JSON 请求体,例如: + +```json +{ + "enabled": true, + "default_action": "ACCEPT", + "rules": [ + { + "id": "", + "network": "ipv4", + "direction": "in", + "protocol": "tcp", + "port": "22", + "source_ip": "", + "action": "ACCEPT", + "description": "Allow SSH", + "enabled": true + } + ] +} +``` + +规则字段说明: + +| 字段 | 说明 | +| --- | --- | +| `network` | 网络范围,常用 `ipv4`,也支持 `ipv6` / `all` | +| `direction` | 方向,`in` 入站,`out` 出站 | +| `protocol` | 协议,`tcp` 或 `udp` | +| `port` | 端口,可填写单端口、逗号分隔端口或端口段,例如 `22`、`80,443`、`8000-9000` | +| `source_ip` | 来源 IP / CIDR,留空表示任意来源 | +| `action` | 动作,`ACCEPT` 放行,`DROP` 拒绝 | +| `description` | 规则描述 | +| `enabled` | 是否启用该规则 | + +IPv4 NAT 入站规则的端口按容器内部端口匹配,不是宿主机公网端口。例如公网 `22023 -> 容器 22`,防火墙规则端口应填写 `22`。 ## WebSSH WebSSH 按钮会调用: @@ -252,6 +313,8 @@ https://www.example.com | 变更资源 | `PUT /api/v1/containers/{name}/resource-limit` | | 变更流量 | `PUT /api/v1/containers/{name}/traffic-limit` | | 同步到期 | `PUT /api/v1/containers/{name}/expiry` | +| 查询防火墙 | `GET /api/v1/containers/{id}/firewall` | +| 更新防火墙 | `PUT /api/v1/containers/{id}/firewall` | | WebSSH | `POST /api/v1/ssh-ticket` | ## 建议 API 权限 @@ -269,6 +332,7 @@ container:password container:traffic container:resize container:port +container:firewall task:read ssh-ticket:create ``` @@ -316,6 +380,22 @@ curl --location --request PUT \ --data-raw '{"container_port":8081,"host_port":61320,"protocol":"tcp","description":"HTTP"}' ``` +查询防火墙: + +```bash +curl -H "X-API-Key: clicd_sk_xxxx" \ + https://0.0.0.0:8999/api/v1/containers/10/firewall +``` + +更新防火墙: + +```bash +curl --location --request PUT \ + "https://0.0.0.0:8999/api/v1/containers/10/firewall" \ + --header "X-API-Key: clicd_sk_xxxx" \ + --header "Content-Type: application/json" \ + --data-raw '{"enabled":true,"default_action":"ACCEPT","rules":[{"id":"","network":"ipv4","direction":"in","protocol":"tcp","port":"22","source_ip":"","action":"ACCEPT","description":"Allow SSH","enabled":true}]}' +``` 创建 WebSSH 票据: ```bash @@ -336,6 +416,41 @@ curl --location --request POST \ Content-Type: application/json ``` +### 防火墙获取提示“不支持的方法” + +请确认模块版本已经包含防火墙页签修复。客户区防火墙列表应通过模块公开的 `firewallList` 调用,再由模块向 CLICD 发起: + +```text +GET /api/v1/containers/{id}/firewall +``` + +如果页面或二开代码直接把读取请求改成 `POST /api/v1/containers/{id}/firewall`,CLICD 会返回“不支持的方法”。 + +### 防火墙保存后规则为空 + +请确认更新接口最终发往 CLICD 的请求体是 JSON,并且包含 `rules` 数组。防火墙关闭时也可以保存规则,`enabled: false` 不应自动清空 `rules`。 + +正确请求体示例: + +```json +{ + "enabled": false, + "default_action": "ACCEPT", + "rules": [ + { + "id": "", + "network": "ipv4", + "direction": "in", + "protocol": "tcp", + "port": "22", + "source_ip": "", + "action": "ACCEPT", + "description": "Allow SSH", + "enabled": true + } + ] +} +``` ### 图表刚打开只有一条横线 CLICD 当前用量接口返回的是实时值,不是历史序列。页面刚打开时只有一个采样点,所以会显示当前值横线。选择 `10 秒` 自动刷新或点击“立即刷新”多采样几次后,会逐步形成折线。 @@ -344,7 +459,66 @@ CLICD 当前用量接口返回的是实时值,不是历史序列。页面刚 旧版本只显示 GB,小流量换算后会被四舍五入成 `0 GB`。当前版本已改为智能单位,会显示 B / KB / MB / GB。 -### WebSSH 打不开或提示不安全 WebSocket +### 防火墙 + +防火墙是独立客户区页面,支持: + +- 查看防火墙启用状态、默认动作和规则列表 +- 启用 / 停用防火墙 +- 设置默认动作:未匹配拒绝或未匹配放行 +- 添加规则 +- 编辑规则 +- 删除规则 +- 单独启用 / 停用某条规则 + +页面会先在前端修改规则列表和开关状态,点击“保存设置”后才统一同步到 CLICD。这样可以避免每次切换开关、修改默认动作或编辑规则时都立即请求后端,减少客户区卡顿。 + +注意:防火墙关闭时也可以保存规则;关闭只表示暂时不接管该容器流量,不代表规则必须清空。 + +使用的 CLICD API: + +```text +GET /api/v1/containers/{id}/firewall +PUT /api/v1/containers/{id}/firewall +``` + +更新防火墙时必须使用 JSON 请求体,例如: + +```json +{ + "enabled": true, + "default_action": "ACCEPT", + "rules": [ + { + "id": "", + "network": "ipv4", + "direction": "in", + "protocol": "tcp", + "port": "22", + "source_ip": "", + "action": "ACCEPT", + "description": "Allow SSH", + "enabled": true + } + ] +} +``` + +规则字段说明: + +| 字段 | 说明 | +| --- | --- | +| `network` | 网络范围,常用 `ipv4`,也支持 `ipv6` / `all` | +| `direction` | 方向,`in` 入站,`out` 出站 | +| `protocol` | 协议,`tcp` 或 `udp` | +| `port` | 端口,可填写单端口、逗号分隔端口或端口段,例如 `22`、`80,443`、`8000-9000` | +| `source_ip` | 来源 IP / CIDR,留空表示任意来源 | +| `action` | 动作,`ACCEPT` 放行,`DROP` 拒绝 | +| `description` | 规则描述 | +| `enabled` | 是否启用该规则 | + +IPv4 NAT 入站规则的端口按容器内部端口匹配,不是宿主机公网端口。例如公网 `22023 -> 容器 22`,防火墙规则端口应填写 `22`。 +## WebSSH 打不开或提示不安全 WebSocket 请确认 CLICD 面板已经启用 HTTPS/WSS,并且魔方服务器配置使用 HTTPS: diff --git a/Mofang/clicd.php b/Mofang/clicd.php index 7c0282a..db09a36 100644 --- a/Mofang/clicd.php +++ b/Mofang/clicd.php @@ -40,7 +40,7 @@ function clicd_MetaData() 'DisplayName' => 'CLICD 对接模块 by 欢-Huan and ChatGPT 5.5 and DeepSeek V4', 'APIVersion' => '1.1', 'HelpDoc' => 'https://github.com/MengMengCode/CLICD', - 'version' => '1.0.5', + 'version' => '1.0.10', ]; } @@ -626,9 +626,24 @@ function clicd_request_value($key, $default = '') function clicd_json_input() { + $input = []; + if (!empty($_POST) && is_array($_POST)) { + $input = $_POST; + } + $raw = file_get_contents('php://input'); $data = json_decode((string)$raw, true); - return is_array($data) ? $data : []; + if (is_array($data)) { + return array_merge($input, $data); + } + + $form = []; + parse_str((string)$raw, $form); + if (!empty($form) && is_array($form)) { + return array_merge($input, $form); + } + + return $input; } function clicd_param_value($data, $key, $default = '') @@ -1689,7 +1704,3 @@ function clicd_ClientAreaOutput($params, $key) ], ]; } - - - - diff --git a/Mofang/templates/firewall.html b/Mofang/templates/firewall.html index 29c0ae5..df836ba 100644 --- a/Mofang/templates/firewall.html +++ b/Mofang/templates/firewall.html @@ -4,32 +4,6 @@ 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; @@ -43,22 +17,60 @@ margin: 18px 0 8px } + .clicd-fw-title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin: 18px 0 8px + } + + .clicd-fw-title-row .clicd-fw-title { + margin: 0 + } + .clicd-fw-muted { color: #6b7280 } - .clicd-fw-toggle-row { + .clicd-fw-settings { + margin-bottom: 22px + } + + .clicd-fw-settings-head { display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 26px + } + + .clicd-fw-settings-title { + font-size: 16px; + font-weight: 700; + color: #111827; + line-height: 1.35 + } + + .clicd-fw-settings-actions { + display: flex; + justify-content: flex-end; align-items: center; - gap: 12px; - margin-bottom: 12px + gap: 10px; + margin-top: 12px + } + + .clicd-fw-save-hint { + color: #6b7280; + font-size: 13px } .clicd-fw-toggle { position: relative; display: inline-flex; - width: 48px; - height: 26px; + width: 56px; + height: 30px; + flex: 0 0 auto; cursor: pointer } @@ -71,8 +83,8 @@ .clicd-fw-toggle-slider { position: absolute; inset: 0; - background: #d1d5db; - border-radius: 26px; + background: #cfd5df; + border-radius: 999px; transition: .25s } @@ -83,16 +95,10 @@ 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 + top: 4px; + left: 4px; + transition: .25s; + box-shadow: 0 1px 3px rgba(15, 23, 42, .18) } .clicd-fw-toggle input:checked+.clicd-fw-toggle-slider { @@ -100,24 +106,29 @@ } .clicd-fw-toggle input:checked+.clicd-fw-toggle-slider:before { - transform: translateX(22px) + transform: translateX(26px) } - .clicd-fw-toggle-label { - font-size: 14px; - font-weight: 500 + .clicd-fw-toggle-sm { + width: 36px; + height: 20px + } + + .clicd-fw-toggle-sm .clicd-fw-toggle-slider:before { + width: 16px; + height: 16px; + top: 2px; + left: 2px + } + + .clicd-fw-toggle-sm input:checked+.clicd-fw-toggle-slider:before { + transform: translateX(16px) } .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 + color: #6b7280; + line-height: 1.5 } .clicd-fw-field label { @@ -143,6 +154,13 @@ outline: none } + .clicd-fw-field-help { + color: #9ca3af; + font-size: 12px; + line-height: 1.5; + margin-top: 6px + } + .clicd-fw-actions { display: flex; gap: 8px; @@ -178,6 +196,12 @@ color: #fff } + .clicd-fw-btn-dark { + border-color: #000; + background: #000; + color: #fff + } + .clicd-fw-btn-sm { height: 30px; padding: 0 10px; @@ -185,26 +209,47 @@ } .clicd-fw-rules { - display: flex; - flex-direction: column; - gap: 10px; - margin-top: 8px + margin-top: 8px; + overflow-x: auto; + background: #fff } - .clicd-fw-rule { - border: 1px solid #e5e7eb; - border-radius: 6px; - background: #fff; - padding: 12px + .clicd-fw-table { + width: 100%; + min-width: 920px; + border-collapse: collapse; + table-layout: fixed } - .clicd-fw-rule-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - margin-bottom: 8px; - flex-wrap: wrap + .clicd-fw-table th, + .clicd-fw-table td { + padding: 13px 14px; + border-bottom: 1px solid #e5e7eb; + text-align: left; + vertical-align: middle; + color: #111827; + font-size: 14px; + white-space: nowrap + } + + .clicd-fw-table th { + background: #f8fafc; + color: #6b7280; + font-weight: 600 + } + + .clicd-fw-table tbody tr:hover { + background: #f9fafb + } + + .clicd-fw-table td:nth-child(8) { + white-space: normal; + word-break: break-word + } + + .clicd-fw-table th:last-child, + .clicd-fw-table td:last-child { + text-align: right } .clicd-fw-rule-direction { @@ -247,34 +292,41 @@ color: #991b1b } - .clicd-fw-rule-desc { - display: flex; + .clicd-fw-rule-network { + display: inline-flex; align-items: center; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 8px - } - - .clicd-fw-rule-detail { - font-size: 13px; + padding: 3px 8px; + border-radius: 4px; + background: #f3f4f6; color: #374151; + font-size: 12px; + font-weight: 600 + } + + .clicd-fw-rule-ops { display: flex; align-items: center; gap: 8px; - flex-wrap: wrap + justify-content: flex-end } - .clicd-fw-rule-detail .sep { - color: #d1d5db + .clicd-fw-icon-btn { + width: 30px; + height: 30px; + border: 0; + border-radius: 4px; + background: transparent; + color: #9ca3af; + cursor: pointer; + font-size: 17px; + line-height: 30px; + padding: 0; + text-align: center } - .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-icon-btn:hover { + background: #f3f4f6; + color: #374151 } .clicd-fw-message { @@ -325,6 +377,60 @@ padding: 16px } + .clicd-fw-rule-modal { + width: min(560px, 100%); + max-height: calc(100vh - 48px); + padding: 0; + display: flex; + flex-direction: column; + overflow: hidden + } + + .clicd-fw-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 20px 26px; + border-bottom: 1px solid #e5e7eb + } + + .clicd-fw-modal-close { + width: 28px; + height: 28px; + border: 0; + background: transparent; + color: #6b7280; + cursor: pointer; + font-size: 28px; + line-height: 24px; + padding: 0 + } + + .clicd-fw-rule-modal-body { + padding: 24px 26px 8px; + overflow-y: auto + } + + .clicd-fw-rule-modal-body .clicd-fw-field { + margin-bottom: 16px + } + + .clicd-fw-rule-modal-body .clicd-fw-input, + .clicd-fw-rule-modal-body .clicd-fw-select { + height: 48px; + border-radius: 6px; + padding: 0 14px; + font-size: 15px + } + + .clicd-fw-rule-modal-body .clicd-fw-field label { + color: #4b5563; + font-size: 14px; + font-weight: 600; + margin-bottom: 8px + } + .clicd-fw-modal-title { font-size: 16px; font-weight: 700; @@ -332,6 +438,10 @@ margin-bottom: 8px } + .clicd-fw-modal-header .clicd-fw-modal-title { + margin-bottom: 0 + } + .clicd-fw-modal-body { font-size: 14px; color: #4b5563; @@ -342,120 +452,202 @@ .clicd-fw-modal-actions { display: flex; justify-content: flex-end; - gap: 8px + gap: 12px + } + + .clicd-fw-rule-modal .clicd-fw-modal-actions { + padding: 6px 26px 24px + } + + .clicd-fw-rule-modal .clicd-fw-modal-actions .clicd-fw-btn { + height: 44px; + min-width: 76px; + font-size: 14px } .clicd-fw-default-action-row { display: flex; align-items: center; - gap: 10px; - margin-top: 10px + justify-content: space-between; + gap: 12px; + border: 1px solid #e5e7eb; + border-radius: 6px; + background: #fff; + padding: 12px 16px } - .clicd-fw-default-action-row label { + .clicd-fw-default-title { + font-size: 15px; + font-weight: 700; + color: #111827; + line-height: 1.35 + } + + .clicd-fw-default-desc { color: #6b7280; - font-size: 12px; - white-space: nowrap + font-size: 13px; + line-height: 1.5 } .clicd-fw-default-action-row select { - height: 30px; + width: auto; + min-width: 126px; + height: 38px; border: 1px solid #d1d5db; border-radius: 4px; - padding: 0 8px; - font-size: 13px + padding: 0 12px; + font-size: 14px; + color: #111827; + background: #fff + } + + .clicd-fw-network-note { + margin-top: 14px; + border: 1px solid #bfdbfe; + border-radius: 6px; + background: #eff6ff; + color: #1d4ed8; + padding: 12px 16px; + line-height: 1.6 + } + + .clicd-fw-network-note-title { + font-weight: 700; + margin-bottom: 2px + } + + @media (max-width: 640px) { + .clicd-fw-default-action-row, + .clicd-fw-settings-actions { + align-items: stretch; + flex-direction: column + } + + .clicd-fw-default-action-row select, + .clicd-fw-settings-actions .clicd-fw-btn { + width: 100% + } + + .clicd-fw-title-row { + align-items: stretch; + flex-direction: column + } + + .clicd-fw-title-row .clicd-fw-btn { + width: 100% + } + + .clicd-fw-modal-header, + .clicd-fw-rule-modal-body, + .clicd-fw-rule-modal .clicd-fw-modal-actions { + padding-left: 18px; + padding-right: 18px + } }
-
-
-
实例名称
-
{$container_name}
-
-
-
IP 地址
-
{$server_ip}
-
-
- -
-
+
+
+
+
防火墙
+
加载中...
+
- 启用防火墙 - 加载中...
- - + +
-
- -
添加规则
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
+
+
网络范围
+
可配置:IPv4(NAT)。 IPv4 规则覆盖 IPv4 NAT 端口映射。 NAT 入站端口按容器内部端口匹配,不是宿主机公网端口。
+
+
+ 当前设置已保存 +
-
防火墙规则
+
+
防火墙规则
+ +
加载中...

 
+    
+
+
+
添加规则
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
NAT 入站填容器内部端口,例如公网 22023 -> 容器 22,这里填 22
+
+
+ + +
留空为任意 IPv4,支持 CIDR: 192.168.1.0/24
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
确认删除
@@ -481,12 +673,23 @@ var enabledCheckbox = document.getElementById('clicd-fw-enabled'); var defaultActionSelect = document.getElementById('clicd-fw-default-action'); var statusText = document.getElementById('clicd-fw-status-text'); + var saveSettingsButton = document.getElementById('clicd-fw-save-settings'); + var saveHint = document.getElementById('clicd-fw-save-hint'); + var addModal = document.getElementById('clicd-fw-add-modal'); + var addClose = document.getElementById('clicd-fw-add-close'); + var addCancel = document.getElementById('clicd-fw-add-cancel'); + var addConfirm = document.getElementById('clicd-fw-add-confirm'); + var ruleModalTitle = document.getElementById('clicd-fw-rule-modal-title'); var deleteModal = document.getElementById('clicd-fw-delete-modal'); var deleteText = document.getElementById('clicd-fw-delete-text'); var deleteCancel = document.getElementById('clicd-fw-delete-cancel'); var deleteConfirm = document.getElementById('clicd-fw-delete-confirm'); var pendingDeleteRule = null; var currentRules = []; + var ruleModalMode = 'add'; + var editingRuleIndex = null; + var settingsDirty = false; + var busy = false; function showMessage(type, text) { message.className = 'clicd-fw-message' + (type === 'error' ? ' error' : ''); @@ -507,8 +710,31 @@ return panel.getAttribute('data-service-id') || ''; } - function setBusy(busy) { - panel.querySelectorAll('button, input, select').forEach(function (el) { el.disabled = !!busy; }); + function updateSaveState() { + if (saveSettingsButton) { + saveSettingsButton.disabled = busy || !settingsDirty; + } + if (saveHint) { + saveHint.textContent = settingsDirty ? '有未保存更改,点击保存设置后生效' : '当前设置已保存'; + } + } + + function setBusy(value) { + busy = !!value; + panel.querySelectorAll('button, input, select').forEach(function (el) { el.disabled = busy; }); + updateSaveState(); + } + + function markDirty(text) { + settingsDirty = true; + updateStatusText(); + updateSaveState(); + showMessage('success', text || '设置已修改,点击保存设置后生效'); + } + + function clearDirty() { + settingsDirty = false; + updateSaveState(); } function escapeHtml(value) { @@ -532,7 +758,7 @@ rulesContainer.innerHTML = '
暂无防火墙规则
'; return; } - rulesContainer.innerHTML = currentRules.map(function (rule, idx) { + var rows = currentRules.map(function (rule, idx) { var dir = (rule.direction || 'in').toLowerCase(); var proto = (rule.protocol || 'tcp').toLowerCase(); var action = (rule.action || 'ACCEPT').toUpperCase(); @@ -540,110 +766,98 @@ var port = escapeHtml(portDisplay(rule.port)); var srcIp = escapeHtml(sourceIpDisplay(rule.source_ip)); var desc = escapeHtml(rule.description || ''); - var ruleId = escapeHtml(rule.id || ''); var enabled = rule.enabled !== false; var enabledChecked = enabled ? 'checked' : ''; var dirClass = dir === 'in' ? 'clicd-fw-direction-in' : 'clicd-fw-direction-out'; var actionClass = 'clicd-fw-action-' + action; - return '
' + - '
' + - '
' + - '' + (dir === 'in' ? '↑ 入站' : '↓ 出站') + '' + - '' + (action === 'ACCEPT' ? '放行' : '拒绝') + '' + - '' + proto.toUpperCase() + '' + - '' + networkLabel(network) + '' + - '' + - (port !== '所有' ? '端口: ' + port : '') + - (srcIp !== '任意' && port !== '所有' ? ' | ' : '') + - (srcIp !== '任意' ? '来源: ' + srcIp : '') + - '' + - '
' + - '
' + - '' + - '
' + - '
' + - '
' + - '' + (desc || '无说明') + '' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '' + - '' + - '
' + - '
' + - '
'; + return '' + + '' + + '' + networkLabel(network) + '' + + '' + (dir === 'in' ? '入站' : '出站') + '' + + '' + escapeHtml(proto.toUpperCase()) + '' + + '' + port + '' + + '' + srcIp + '' + + '' + (action === 'ACCEPT' ? '放行' : '拒绝') + '' + + '' + (desc || '-') + '' + + '
' + + '' + + '' + + '
' + + ''; }).join(''); + rulesContainer.innerHTML = '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '
状态网络方向协议端口来源/目标 IP动作描述操作
'; + rulesContainer.querySelectorAll('.clicd-fw-rule-enabled').forEach(function (toggle, idx) { toggle.addEventListener('change', function () { var rule = currentRules[idx]; if (!rule) return; rule.enabled = toggle.checked; - saveFirewall(); + markDirty('规则开关已修改,点击保存设置后生效'); }); }); } - function getEditField(item, name) { - return item.querySelector('[data-field="' + name + '"]'); + function setRuleField(id, value) { + var field = document.getElementById(id); + if (field) { + field.value = value == null ? '' : value; + } } - function updateRuleFromFields(item, idx) { - currentRules[idx].network = getEditField(item, 'network') ? getEditField(item, 'network').value : 'ipv4'; - currentRules[idx].direction = getEditField(item, 'direction') ? getEditField(item, 'direction').value : 'in'; - currentRules[idx].protocol = getEditField(item, 'protocol') ? getEditField(item, 'protocol').value : 'tcp'; - currentRules[idx].port = getEditField(item, 'port') ? getEditField(item, 'port').value : ''; - currentRules[idx].source_ip = getEditField(item, 'source_ip') ? getEditField(item, 'source_ip').value : ''; - currentRules[idx].action = getEditField(item, 'action') ? getEditField(item, 'action').value : 'ACCEPT'; - currentRules[idx].description = getEditField(item, 'description') ? getEditField(item, 'description').value : ''; + function getRuleField(id, fallback) { + var field = document.getElementById(id); + if (!field) return fallback || ''; + return field.value; } - function getAddRulePayload() { + function getRuleFormPayload(baseRule) { + var base = baseRule || {}; return { - id: '', - network: document.getElementById('clicd-fw-add-network').value, - direction: document.getElementById('clicd-fw-add-direction').value, - protocol: document.getElementById('clicd-fw-add-protocol').value, - port: document.getElementById('clicd-fw-add-port').value, - source_ip: document.getElementById('clicd-fw-add-source-ip').value, - action: document.getElementById('clicd-fw-add-action').value, - description: document.getElementById('clicd-fw-add-desc').value, - enabled: true + id: base.id || '', + network: getRuleField('clicd-fw-add-network', 'ipv4'), + direction: getRuleField('clicd-fw-add-direction', 'in'), + protocol: getRuleField('clicd-fw-add-protocol', 'tcp'), + port: getRuleField('clicd-fw-add-port', ''), + source_ip: getRuleField('clicd-fw-add-source-ip', ''), + action: getRuleField('clicd-fw-add-action', 'DROP'), + description: getRuleField('clicd-fw-add-desc', ''), + enabled: baseRule ? base.enabled !== false : true }; } function clearAddForm() { - document.getElementById('clicd-fw-add-network').value = 'ipv4'; - document.getElementById('clicd-fw-add-port').value = ''; - document.getElementById('clicd-fw-add-source-ip').value = ''; - document.getElementById('clicd-fw-add-action').value = 'ACCEPT'; - document.getElementById('clicd-fw-add-desc').value = ''; + setRuleField('clicd-fw-add-network', 'ipv4'); + setRuleField('clicd-fw-add-direction', 'in'); + setRuleField('clicd-fw-add-protocol', 'tcp'); + setRuleField('clicd-fw-add-port', ''); + setRuleField('clicd-fw-add-source-ip', ''); + setRuleField('clicd-fw-add-action', 'DROP'); + setRuleField('clicd-fw-add-desc', ''); + } + + function fillRuleForm(rule) { + var item = rule || {}; + setRuleField('clicd-fw-add-network', item.network || 'ipv4'); + setRuleField('clicd-fw-add-direction', item.direction || 'in'); + setRuleField('clicd-fw-add-protocol', item.protocol || 'tcp'); + setRuleField('clicd-fw-add-port', item.port || ''); + setRuleField('clicd-fw-add-source-ip', item.source_ip || ''); + setRuleField('clicd-fw-add-action', item.action || 'DROP'); + setRuleField('clicd-fw-add-desc', item.description || ''); } - // ── FIX: 发 JSON body,走 firewallajax 路由 ── function saveFirewall() { var enabled = enabledCheckbox.checked; var defaultAction = defaultActionSelect.value; @@ -661,22 +875,22 @@ }; }); + var body = new URLSearchParams(); + body.set('id', serviceId()); + body.set('func', 'firewallUpdate'); + body.set('enabled', enabled ? 'true' : 'false'); + body.set('default_action', defaultAction); + body.set('rules', JSON.stringify(rules)); + setBusy(true); fetch(endpoint(), { method: 'POST', headers: { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Authorization': 'JWT {$Think.get.jwt}' }, credentials: 'same-origin', - body: JSON.stringify({ - id: serviceId(), - func: 'firewallajax', - action: 'update', - enabled: enabled, - default_action: defaultAction, - rules: rules - }) + body: body.toString() }) .then(function (res) { return res.text(); }) .then(function (text) { @@ -685,10 +899,17 @@ showDebug((data.data && data.data.debug) || data.debug || data); if (data.status === 200 || data.status === 'success') { showMessage('success', data.msg || '防火墙设置已更新'); + if (data.data && Object.prototype.hasOwnProperty.call(data.data, 'enabled')) { + enabledCheckbox.checked = data.data.enabled === true || data.data.enabled === 'true' || data.data.enabled === 1; + } + if (data.data && data.data.default_action && defaultActionSelect) { + defaultActionSelect.value = data.data.default_action; + } if (data.data && Array.isArray(data.data.rules)) { currentRules = data.data.rules; renderRules(currentRules); } + clearDirty(); updateStatusText(); } else { showMessage('error', data.msg || '更新失败'); @@ -703,21 +924,20 @@ }); } - // ── FIX: 发 JSON body,走 firewallajax 路由 ── function loadFirewall() { + var body = new URLSearchParams(); + body.set('id', serviceId()); + body.set('func', 'firewallList'); + setBusy(true); fetch(endpoint(), { method: 'POST', headers: { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Authorization': 'JWT {$Think.get.jwt}' }, credentials: 'same-origin', - body: JSON.stringify({ - id: serviceId(), - func: 'firewallajax', - action: 'list' - }) + body: body.toString() }) .then(function (res) { return res.text(); }) .then(function (text) { @@ -732,6 +952,7 @@ } currentRules = Array.isArray(data.data.rules) ? data.data.rules : []; renderRules(currentRules); + clearDirty(); updateStatusText(); } } else { @@ -751,9 +972,9 @@ function updateStatusText() { if (enabledCheckbox.checked) { - statusText.textContent = '已启用 - 默认' + (defaultActionSelect.value === 'DROP' ? '拒绝' : '放行') + '未匹配流量'; + statusText.textContent = '已启用,未匹配规则的流量将被' + (defaultActionSelect.value === 'DROP' ? '拒绝' : '放行'); } else { - statusText.textContent = '已禁用 - 所有流量不受限制'; + statusText.textContent = '未启用时不接管该容器流量'; } } @@ -774,27 +995,51 @@ } } + function openRuleModal(mode, idx) { + ruleModalMode = mode === 'edit' ? 'edit' : 'add'; + editingRuleIndex = ruleModalMode === 'edit' ? idx : null; + if (ruleModalTitle) { + ruleModalTitle.textContent = ruleModalMode === 'edit' ? '编辑规则' : '添加规则'; + } + if (ruleModalMode === 'edit' && currentRules[idx]) { + fillRuleForm(currentRules[idx]); + } else { + clearAddForm(); + } + if (addModal) { + addModal.style.display = 'flex'; + } + var portInput = document.getElementById('clicd-fw-add-port'); + if (portInput) { + setTimeout(function () { portInput.focus(); }, 0); + } + } + + function closeAddModal() { + ruleModalMode = 'add'; + editingRuleIndex = null; + if (addModal) { + addModal.style.display = 'none'; + } + } + panel.addEventListener('click', function (event) { var button = event.target.closest('[data-clicd-fw-action]'); if (!button) return; var action = button.getAttribute('data-clicd-fw-action'); - if (action === 'add-rule') { - currentRules.push(getAddRulePayload()); - renderRules(currentRules); - clearAddForm(); - saveFirewall(); + if (action === 'open-add-rule') { + openRuleModal('add'); return; } - var item = button.closest('.clicd-fw-rule'); + var item = button.closest('[data-rule-index]'); if (!item) return; var idx = parseInt(item.getAttribute('data-rule-index'), 10); if (isNaN(idx) || !currentRules[idx]) return; - if (action === 'update-rule') { - updateRuleFromFields(item, idx); - saveFirewall(); + if (action === 'edit-rule') { + openRuleModal('edit', idx); return; } @@ -807,14 +1052,46 @@ enabledCheckbox.addEventListener('change', function () { updateStatusText(); - saveFirewall(); + markDirty('防火墙开关已修改,点击保存设置后生效'); }); defaultActionSelect.addEventListener('change', function () { updateStatusText(); - saveFirewall(); + markDirty('默认动作已修改,点击保存设置后生效'); }); + if (saveSettingsButton) { + saveSettingsButton.addEventListener('click', saveFirewall); + } + + if (addClose) { + addClose.addEventListener('click', closeAddModal); + } + if (addCancel) { + addCancel.addEventListener('click', closeAddModal); + } + if (addModal) { + addModal.addEventListener('click', function (event) { + if (event.target === addModal) closeAddModal(); + }); + } + if (addConfirm) { + addConfirm.addEventListener('click', function () { + if (ruleModalMode === 'edit' && editingRuleIndex !== null && currentRules[editingRuleIndex]) { + currentRules[editingRuleIndex] = getRuleFormPayload(currentRules[editingRuleIndex]); + renderRules(currentRules); + closeAddModal(); + markDirty('规则已更新,点击保存设置后生效'); + return; + } + currentRules.push(getRuleFormPayload(null)); + renderRules(currentRules); + closeAddModal(); + clearAddForm(); + markDirty('规则已添加,点击保存设置后生效'); + }); + } + if (deleteCancel) { deleteCancel.addEventListener('click', closeDeleteModal); } @@ -831,11 +1108,12 @@ if (idx >= 0 && idx < currentRules.length) { currentRules.splice(idx, 1); renderRules(currentRules); - saveFirewall(); + markDirty('规则已删除,点击保存设置后生效'); } }); } + updateSaveState(); loadFirewall(); })(); - \ No newline at end of file +