Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a7f679fca | |||
| 8b6b94ea1f | |||
| 0d9c8b3a2d | |||
| 2be97a4543 | |||
| 7b6b86f0f6 | |||
| 6f82242d92 | |||
| 7f301cec58 | |||
| c75cfe9eb6 | |||
| b94664ed87 | |||
| 93fc5e08f6 | |||
| 9bcfd4cd73 | |||
| 2d10110eef | |||
| 78e31fdc40 | |||
| d2b1394950 | |||
| 389b0a005b | |||
| 0b1554c2c6 | |||
| 3b580e2827 | |||
| 595f5ba2d9 | |||
| 78543dac67 | |||
| e9cbc8e945 | |||
| 905e206f9c | |||
| b5b1992e3b | |||
| a7fe0c87a5 | |||
| 5dcbac1c93 | |||
| 67a6d5cea8 | |||
| f6fec06e82 | |||
| 2d74e8966e | |||
| 065877547e | |||
| 12b6a1af35 | |||
| b36f7f3085 | |||
| 2112e86329 | |||
| f2c3569447 | |||
| 1a9fe40b80 | |||
| df021819b4 | |||
| 57749f03b9 | |||
| d97697efde | |||
| bc28719b10 | |||
| db79d17919 | |||
| 4e6654af33 | |||
| 4015c7e1bc | |||
| d30874a5d4 | |||
| f4a97c1740 | |||
| 7cb497929b | |||
| 8cb74159f2 | |||
| 3b75f7d2ac | |||
| 87c65176aa | |||
| 3b3cb1ff24 | |||
| a701ebcad6 | |||
| 3d88983feb | |||
| 39a9bad2e1 | |||
| 7b0e98bf02 | |||
| 324c61625d | |||
| b50a3f4b4e | |||
| 3de3b40d88 | |||
| 722c4f3812 | |||
| 9cf3a0e2b6 | |||
| c80c22c133 | |||
| 5acf1e6eb7 | |||
| cdeaf9f48f | |||
| afebeb7c00 | |||
| 0d73f2919d | |||
| 070ad97e95 | |||
| b7eba92b2b | |||
| 2c339b9923 | |||
| 8d0ea50d4f | |||
| d6a1c13328 | |||
| 789e6e2043 | |||
| d692b9ac13 | |||
| eb362e42e3 | |||
| 3ca106ec87 | |||
| 377baa0783 | |||
| 117ccabbb5 | |||
| b718c11f3a | |||
| 41d3d3fb97 | |||
| 31ad944da8 | |||
| a25de6e4db | |||
| 3af6d4d47d | |||
| 59a2fef9e8 | |||
| 3d22a1bc03 | |||
| e64c27a821 | |||
| f2bdba10bb | |||
| e18d07e694 | |||
| 50ba0ce38d | |||
| 8b2c7ea778 | |||
| e640a3cedd | |||
| 31ab4f0fb6 | |||
| 8dee17b539 | |||
| af3172739e | |||
| 182d9b1c8c | |||
| d03facb556 |
@@ -27,7 +27,7 @@ body:
|
||||
required: false
|
||||
- label: 这个问题可以被稳定复现 (The problem can be stably reproduced)
|
||||
required: false
|
||||
- label: 问题是在升级之后产生的 (The problem is generated after upgrading)
|
||||
- label: 问题是在更新之后产生的 (The problem is generated after upgrading)
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: system
|
||||
@@ -68,7 +68,7 @@ body:
|
||||
attributes:
|
||||
label: 描述问题 (Describe The Problem)
|
||||
description: |
|
||||
简明概要地描述你遇到的问题。
|
||||
简明概要地描述您遇到的问题。
|
||||
Briefly describe the problem you are having.
|
||||
validations:
|
||||
required: true
|
||||
@@ -91,7 +91,7 @@ body:
|
||||
attributes:
|
||||
label: 预期行为 (Expected Behavior)
|
||||
description: |
|
||||
简明概要地描述你期望发生的事情。
|
||||
简明概要地描述您期望发生的事情。
|
||||
Briefly describe what you expect to happen.
|
||||
validations:
|
||||
required: true
|
||||
@@ -113,7 +113,7 @@ body:
|
||||
attributes:
|
||||
label: 截图 (Screenshots)
|
||||
description: |
|
||||
如果有,添加屏幕截图可帮助更快定位你的问题。
|
||||
如果有,添加屏幕截图可帮助更快定位您的问题。
|
||||
If so, adding screenshots can help locate your issue faster.
|
||||
可以复制图片后在此区域内粘贴以添加图片。
|
||||
Pictures can be copied and pasted in this area to add pictures.
|
||||
@@ -128,7 +128,7 @@ body:
|
||||
description: |
|
||||
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
|
||||
Environment? Browser? Software version? Related configuration? Link? References?
|
||||
任何能让我们对你所遇到的问题有更多了解的东西。
|
||||
任何能让我们对您所遇到的问题有更多了解的东西。
|
||||
Anything that can give us more insight into the problem you're having.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -25,7 +25,7 @@ body:
|
||||
attributes:
|
||||
label: 描述功能 (Describe Feature)
|
||||
description: |
|
||||
简明概要地描述你的新功能,以及它将解决什么问题。
|
||||
简明概要地描述您的新功能,以及它将解决什么问题。
|
||||
Briefly describe your new feature and what problem it will solve.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 819 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Run tests with coverage
|
||||
run: go test -v -coverprofile="coverage.out" ./...
|
||||
run: sudo go test -v -coverprofile="coverage.out" ./...
|
||||
- name: Upload coverage report to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Issue Auto Lock
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */6 * * *"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
issues:
|
||||
types: [ opened ]
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
jobs:
|
||||
issue-lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set GH_REPO
|
||||
run: echo "GH_REPO=${{ github.repository }}" >> $GITHUB_ENV
|
||||
- name: Auto Lock Issue
|
||||
uses: devhaozi/issue-auto-lock@v1
|
||||
with:
|
||||
gh_repo: ${{ github.repository }}
|
||||
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue_labels: "⭐ No Star"
|
||||
@@ -21,4 +21,4 @@ jobs:
|
||||
run: |
|
||||
cp config.example.yml config.yml
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
run: sudo go test ./...
|
||||
@@ -26,13 +26,13 @@
|
||||
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
|
||||
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
|
||||
4. **高效运维:** 面板界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
|
||||
5. **离线运行:** 面板运行可不依赖任何外部服务,你甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||
5. **离线运行:** 面板运行可不依赖任何外部服务,您甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 2 年无事故。
|
||||
7. **开源开放:** 面板开源,你可以自由修改、审计面板源码,安全性有保障。
|
||||
7. **开源开放:** 面板开源,您可以自由修改、审计面板源码,安全性有保障。
|
||||
|
||||
## UI 截图
|
||||
|
||||

|
||||

|
||||
|
||||
## 运行环境
|
||||
|
||||
@@ -46,28 +46,35 @@
|
||||
|---------------------|-----|-----|
|
||||
| AlmaLinux | 9 | 推荐 |
|
||||
| AlmaLinux | 8 | 不推荐 |
|
||||
| RockyLinux | 9 | |
|
||||
| RockyLinux | 9 | 支持 |
|
||||
| RockyLinux | 8 | 不推荐 |
|
||||
| CentOS Stream | 9 | 不推荐 |
|
||||
| CentOS Stream | 8 | 不推荐 |
|
||||
| Ubuntu | 24 | 推荐 |
|
||||
| Ubuntu | 22 | |
|
||||
| Ubuntu | 22 | 支持 |
|
||||
| Debian | 12 | 推荐 |
|
||||
| Debian | 11 | |
|
||||
| OpenCloudOS | 9 | |
|
||||
| TencentOS Server | 4 | |
|
||||
| Debian | 11 | 支持 |
|
||||
| OpenCloudOS | 9 | 支持 |
|
||||
| TencentOS Server | 4 | 支持 |
|
||||
| TencentOS Server | 3.1 | 不推荐 |
|
||||
| Alibaba Cloud Linux | 3.2 | 不推荐 |
|
||||
| Anolis | 8 | 不推荐 |
|
||||
| openEuler | 22 | 不推荐 |
|
||||
|
||||
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的健壮性。
|
||||
|
||||
## 安装面板
|
||||
|
||||
> **Warning**
|
||||
> 安装面板前,你需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
|
||||
> 安装面板前,您需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
|
||||
|
||||
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
|
||||
如果您的服务器有未挂载的数据盘,可在安装前以`root`用户登录服务器运行以下命令自动挂载,面板安装后不支持跨目录迁移。
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
|
||||
```
|
||||
|
||||
准备就绪后,请以`root`用户登录服务器,运行以下命令安装面板:
|
||||
|
||||
**当前 v2.3.x 为测试版本,欢迎帮助我们测试在不同操作系统下的兼容性。**
|
||||
|
||||
@@ -79,7 +86,7 @@ curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash in
|
||||
|
||||
优先建议备份数据重装系统,这样可以保证系统纯净。
|
||||
|
||||
如果你无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||
如果您无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
|
||||
@@ -105,19 +112,35 @@ panel-cli
|
||||
|
||||
## 赞助商
|
||||
|
||||
如果耗子面板对你有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
|
||||
如果耗子面板对您有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
|
||||
|
||||
**同时接受云资源赞助,可通过QQ群咨询联系**
|
||||
|
||||
### 资金
|
||||
|
||||
- [微晓朵](https://www.weixiaoduo.com/)
|
||||
<a href="https://www.weixiaoduo.com/">
|
||||
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
|
||||
</a>
|
||||
|
||||
### 服务器
|
||||
|
||||
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
|
||||
<img height="80" src=".github/assets/dk.png" alt="林枫云">
|
||||
</a>
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
- [WAF PRO](https://waf.pro/)
|
||||
- [盾云SCDN](https://scdn.ddunyun.com/)
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
|
||||
</a>
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
|
||||
</a>
|
||||
<a href="https://scdn.ddunyun.com/">
|
||||
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
|
||||
</a>
|
||||
|
||||
### 赞助商们
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
||||
|
||||
@@ -32,7 +32,7 @@ Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | For
|
||||
|
||||
## UI Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
## Operating Environment
|
||||
|
||||
@@ -46,19 +46,20 @@ For other systems not listed in the table below, you can try to install them by
|
||||
|---------------------|---------|-----------------|
|
||||
| AlmaLinux | 9 | Recommended |
|
||||
| AlmaLinux | 8 | Not recommended |
|
||||
| RockyLinux | 9 | |
|
||||
| RockyLinux | 9 | Support |
|
||||
| RockyLinux | 8 | Not recommended |
|
||||
| CentOS Stream | 9 | Not recommended |
|
||||
| CentOS Stream | 8 | Not recommended |
|
||||
| Ubuntu | 24 | Recommended |
|
||||
| Ubuntu | 22 | |
|
||||
| Ubuntu | 22 | Support |
|
||||
| Debian | 12 | Recommended |
|
||||
| Debian | 11 | |
|
||||
| OpenCloudOS | 9 | |
|
||||
| TencentOS Server | 4 | |
|
||||
| Debian | 11 | Support |
|
||||
| OpenCloudOS | 9 | Support |
|
||||
| TencentOS Server | 4 | Support |
|
||||
| TencentOS Server | 3.1 | Not recommended |
|
||||
| Alibaba Cloud Linux | 3.2 | Not recommended |
|
||||
| Anolis | 8 | Not recommended |
|
||||
| openEuler | 22 | Not recommended |
|
||||
|
||||
As system versions are constantly updated, we may also terminate support for some older systems to ensure the robustness of the panel.
|
||||
|
||||
@@ -67,7 +68,13 @@ As system versions are constantly updated, we may also terminate support for som
|
||||
> **Warning**
|
||||
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. We are not recommended for users with zero basic knowledge to install and use Rat Panel.
|
||||
|
||||
If you decide to continue, please log in to the server as `root` user and execute the following command to install the panel:
|
||||
If your server has an unmounted data disk, you can run the following command as the `root` user to automatically mount it before installation. The panel does not support cross-directory migration after installation.
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
|
||||
```
|
||||
|
||||
After you are ready, log in to the server as the `root` user and run the following command to install the panel:
|
||||
|
||||
**Current v2.3.x is a test version, welcome to help us test compatibility on different operating systems.**
|
||||
|
||||
@@ -111,13 +118,29 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
|
||||
|
||||
### Financial
|
||||
|
||||
- [微晓朵](https://www.weixiaoduo.com/)
|
||||
<a href="https://www.weixiaoduo.com/">
|
||||
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
|
||||
</a>
|
||||
|
||||
### Server
|
||||
|
||||
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
|
||||
<img height="80" src=".github/assets/dk.png" alt="林枫云">
|
||||
</a>
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
- [WAF PRO](https://waf.pro/)
|
||||
- [盾云SCDN](https://scdn.ddunyun.com/)
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
|
||||
</a>
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
|
||||
</a>
|
||||
<a href="https://scdn.ddunyun.com/">
|
||||
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
|
||||
</a>
|
||||
|
||||
### Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
||||
|
||||
@@ -29,12 +29,13 @@ require (
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/libdns/alidns v1.0.3
|
||||
github.com/libdns/cloudflare v0.1.1
|
||||
github.com/libdns/dnspod v0.0.3
|
||||
github.com/libdns/huaweicloud v0.2.2
|
||||
github.com/libdns/libdns v0.2.2
|
||||
github.com/libdns/tencentcloud v1.0.0
|
||||
github.com/mholt/acmez/v2 v2.0.3
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.8
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/sethvargo/go-limiter v1.0.0
|
||||
github.com/shirou/gopsutil v2.21.11+incompatible
|
||||
github.com/spf13/cast v1.7.0
|
||||
@@ -59,6 +60,7 @@ require (
|
||||
github.com/connesc/cipherio v0.2.1 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
@@ -95,7 +97,6 @@ require (
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
@@ -103,7 +104,6 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/samber/lo v1.47.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
|
||||
@@ -50,6 +50,8 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 h1:AjCMD0AEGHHbcIpvRcrI8rMA+Us5F4uUt3B3KSpiYN8=
|
||||
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7/go.mod h1:Gz/Ng6WCvYc1Q2smuPvow9kszfyH47wtnpZzvdD2juA=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
@@ -218,10 +220,9 @@ github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=
|
||||
github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
|
||||
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
|
||||
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
|
||||
github.com/libdns/dnspod v0.0.3 h1:xJHDIujgLjvZnpB8/rMoCHUqA/KxSGBqRUXxSIzNzAA=
|
||||
github.com/libdns/dnspod v0.0.3/go.mod h1:XLnqMmK7QlLPEbHwcOxbRvlzRvDgaaUlthRNFOPjXPI=
|
||||
github.com/libdns/huaweicloud v0.2.2 h1:DvaWLiPJ0hoOvvPafKXgWv4gG33O8cqEu87GJeXYtbc=
|
||||
github.com/libdns/huaweicloud v0.2.2/go.mod h1:F9vNIca+Cq1ZJiMCSkHQUdvXEGY6UbLoaYI3Py9CFDs=
|
||||
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/libdns/tencentcloud v1.0.0 h1:u4LXnYu/lu/9P5W+MCVPeSDnwI+6w+DxYhQ1wSnQOuU=
|
||||
@@ -247,8 +248,6 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=
|
||||
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -284,11 +283,15 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 h1:C0GHdLTfikLVoEzfhgPfrZ7LwlG0xiCmk6iwNKE+xs0=
|
||||
@@ -299,8 +302,6 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20240907135031-d38eb71142ac h1:IXccMEFcB+UqGWae8OF9EoA0/8GCLlDj6s84LCU7y58=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20240907135031-d38eb71142ac/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8 h1:7yw1yHkylDcu/CwY4hEEe8MycDLo7B9LjcqwhoL8NrM=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
@@ -313,6 +314,7 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@@ -353,6 +355,9 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -382,6 +387,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -400,7 +407,12 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -416,6 +428,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -438,10 +452,22 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -449,6 +475,11 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -482,6 +513,8 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -25,9 +25,20 @@ var (
|
||||
Logger *zap.Logger
|
||||
)
|
||||
|
||||
// 定义面板状态常量
|
||||
const (
|
||||
StatusNormal = iota
|
||||
StatusMaintain
|
||||
StatusClosed
|
||||
StatusUpgrade
|
||||
StatusFailed
|
||||
)
|
||||
|
||||
// 面板全局变量
|
||||
var (
|
||||
Root string
|
||||
Version string
|
||||
Locale string
|
||||
IsCli bool
|
||||
Status = StatusNormal
|
||||
)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "benchmark",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Post("/test", service.Test)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package benchmark
|
||||
|
||||
type Test struct {
|
||||
Name string `json:"name" validate:"required,oneof=image machine compile encryption compression physics json memory disk"`
|
||||
Multi bool `json:"multi"`
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// Test 运行测试
|
||||
func (s *Service) Test(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Test](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Name {
|
||||
case "image":
|
||||
result := s.imageProcessing(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "machine":
|
||||
result := s.machineLearning(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "compile":
|
||||
result := s.compileSimulationSingle(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "encryption":
|
||||
result := s.encryptionTest(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "compression":
|
||||
result := s.compressionTest(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "physics":
|
||||
result := s.physicsSimulation(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "json":
|
||||
result := s.jsonProcessing(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "disk":
|
||||
result := s.diskTestTask()
|
||||
service.Success(w, result)
|
||||
case "memory":
|
||||
result := s.memoryTestTask()
|
||||
service.Success(w, result)
|
||||
default:
|
||||
service.Error(w, http.StatusUnprocessableEntity, "未知测试类型")
|
||||
}
|
||||
}
|
||||
|
||||
// calculateCpuScore 计算CPU成绩
|
||||
func (s *Service) calculateCpuScore(duration time.Duration) int {
|
||||
score := int((10 / duration.Seconds()) * float64(3000))
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// calculateScore 计算内存/硬盘成绩
|
||||
func (s *Service) calculateScore(duration time.Duration) int {
|
||||
score := int((20 / duration.Seconds()) * float64(30000))
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// 图像处理
|
||||
|
||||
func (s *Service) imageProcessing(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
if err := s.imageProcessingTask(n); err != nil {
|
||||
return 0
|
||||
}
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) imageProcessingTask(numThreads int) error {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 4000, 4000))
|
||||
for x := 0; x < 4000; x++ {
|
||||
for y := 0; y < 4000; y++ {
|
||||
img.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), A: 255})
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
dx := img.Bounds().Dx()
|
||||
dy := img.Bounds().Dy()
|
||||
chunkSize := dy / numThreads
|
||||
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
startY := i * chunkSize
|
||||
endY := startY + chunkSize
|
||||
if i == numThreads-1 {
|
||||
endY = dy
|
||||
}
|
||||
for x := 1; x < dx-1; x++ {
|
||||
for y := startY + 1; y < endY-1; y++ {
|
||||
// 卷积操作(模糊)
|
||||
rTotal, gTotal, bTotal := 0, 0, 0
|
||||
for k := -1; k <= 1; k++ {
|
||||
for l := -1; l <= 1; l++ {
|
||||
r, g, b, _ := img.At(x+k, y+l).RGBA()
|
||||
rTotal += int(r)
|
||||
gTotal += int(g)
|
||||
bTotal += int(b)
|
||||
}
|
||||
}
|
||||
rAvg := uint8(rTotal / 9 / 256)
|
||||
gAvg := uint8(gTotal / 9 / 256)
|
||||
bAvg := uint8(bTotal / 9 / 256)
|
||||
img.Set(x, y, color.RGBA{R: rAvg, G: gAvg, B: bAvg, A: 255})
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 机器学习(矩阵乘法)
|
||||
|
||||
func (s *Service) machineLearning(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.machineLearningTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) machineLearningTask(numThreads int) {
|
||||
size := 900
|
||||
a := make([][]float64, size)
|
||||
b := make([][]float64, size)
|
||||
for i := 0; i < size; i++ {
|
||||
a[i] = make([]float64, size)
|
||||
b[i] = make([]float64, size)
|
||||
for j := 0; j < size; j++ {
|
||||
a[i][j] = rand.Float64()
|
||||
b[i][j] = rand.Float64()
|
||||
}
|
||||
}
|
||||
|
||||
c := make([][]float64, size)
|
||||
for i := 0; i < size; i++ {
|
||||
c[i] = make([]float64, size)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
chunkSize := size / numThreads
|
||||
|
||||
for k := 0; k < numThreads; k++ {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
start := k * chunkSize
|
||||
end := start + chunkSize
|
||||
if k == numThreads-1 {
|
||||
end = size
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
for j := 0; j < size; j++ {
|
||||
sum := 0.0
|
||||
for l := 0; l < size; l++ {
|
||||
sum += a[i][l] * b[l][j]
|
||||
}
|
||||
c[i][j] = sum
|
||||
}
|
||||
}
|
||||
}(k)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 数学问题(计算斐波那契数)
|
||||
|
||||
func (s *Service) compileSimulationSingle(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
totalCalculations := 1000
|
||||
fibNumber := 20000
|
||||
|
||||
calculationsPerThread := totalCalculations / n
|
||||
remainder := totalCalculations % n
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
tasks := calculationsPerThread
|
||||
if i < remainder {
|
||||
tasks++ // 处理无法均分的剩余任务
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(tasks int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < tasks; j++ {
|
||||
s.fib(fibNumber)
|
||||
}
|
||||
}(tasks)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
// 斐波那契函数
|
||||
func (s *Service) fib(n int) *big.Int {
|
||||
if n < 2 {
|
||||
return big.NewInt(int64(n))
|
||||
}
|
||||
a := big.NewInt(0)
|
||||
b := big.NewInt(1)
|
||||
temp := big.NewInt(0)
|
||||
for i := 2; i <= n; i++ {
|
||||
temp.Add(a, b)
|
||||
a.Set(b)
|
||||
b.Set(temp)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// AES加密
|
||||
|
||||
func (s *Service) encryptionTest(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
if err := s.encryptionTestTask(n); err != nil {
|
||||
return 0
|
||||
}
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) encryptionTestTask(numThreads int) error {
|
||||
key := []byte("abcdefghijklmnopqrstuvwxyz123456")
|
||||
dataSize := 1 * 1024 * 1024 * 1024 // 1GB
|
||||
plaintext := []byte(strings.Repeat("A", dataSize))
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chunkSize := dataSize / numThreads
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
if i == numThreads-1 {
|
||||
end = dataSize
|
||||
}
|
||||
|
||||
nonce := make([]byte, aesGCM.NonceSize())
|
||||
if _, err = cryptorand.Read(nonce); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
aesGCM.Seal(nil, nonce, plaintext[start:end], nil)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 压缩/解压缩
|
||||
|
||||
func (s *Service) compressionTest(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.compressionTestTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) compressionTestTask(numThreads int) {
|
||||
data := []byte(strings.Repeat("耗子面板", 50000000))
|
||||
chunkSize := len(data) / numThreads
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
compressedChunks := make([]bytes.Buffer, numThreads)
|
||||
|
||||
// 压缩
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
if i == numThreads-1 {
|
||||
end = len(data)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
zw, _ := zstd.NewWriter(&buf)
|
||||
_, _ = zw.Write(data[start:end])
|
||||
_ = zw.Close()
|
||||
compressedChunks[i] = buf
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 解压缩
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
zr, err := zstd.NewReader(&compressedChunks[i])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(io.Discard, zr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
zr.Close()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 物理仿真(N体问题)
|
||||
|
||||
func (s *Service) physicsSimulation(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.physicsSimulationTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) physicsSimulationTask(numThreads int) {
|
||||
const (
|
||||
numBodies = 4000
|
||||
steps = 30
|
||||
)
|
||||
|
||||
type Body struct {
|
||||
x, y, z, vx, vy, vz float64
|
||||
}
|
||||
|
||||
bodies := make([]Body, numBodies)
|
||||
for i := 0; i < numBodies; i++ {
|
||||
bodies[i] = Body{
|
||||
x: rand.Float64(),
|
||||
y: rand.Float64(),
|
||||
z: rand.Float64(),
|
||||
vx: rand.Float64(),
|
||||
vy: rand.Float64(),
|
||||
vz: rand.Float64(),
|
||||
}
|
||||
}
|
||||
|
||||
chunkSize := numBodies / numThreads
|
||||
|
||||
for step := 0; step < steps; step++ {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 更新速度
|
||||
for k := 0; k < numThreads; k++ {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
start := k * chunkSize
|
||||
end := start + chunkSize
|
||||
if k == numThreads-1 {
|
||||
end = numBodies
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
bi := &bodies[i]
|
||||
for j := 0; j < numBodies; j++ {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
bj := &bodies[j]
|
||||
dx := bj.x - bi.x
|
||||
dy := bj.y - bi.y
|
||||
dz := bj.z - bi.z
|
||||
dist := math.Sqrt(dx*dx + dy*dy + dz*dz)
|
||||
if dist == 0 {
|
||||
continue
|
||||
}
|
||||
force := 1 / (dist * dist)
|
||||
bi.vx += force * dx / dist
|
||||
bi.vy += force * dy / dist
|
||||
bi.vz += force * dz / dist
|
||||
}
|
||||
}
|
||||
}(k)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 更新位置
|
||||
for k := 0; k < numThreads; k++ {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
start := k * chunkSize
|
||||
end := start + chunkSize
|
||||
if k == numThreads-1 {
|
||||
end = numBodies
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
bi := &bodies[i]
|
||||
bi.x += bi.vx
|
||||
bi.y += bi.vy
|
||||
bi.z += bi.vz
|
||||
}
|
||||
}(k)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// JSON解析
|
||||
|
||||
func (s *Service) jsonProcessing(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.jsonProcessingTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) jsonProcessingTask(numThreads int) {
|
||||
numElements := 1000000
|
||||
elementsPerThread := numElements / numThreads
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
start := i * elementsPerThread
|
||||
end := start + elementsPerThread
|
||||
if i == numThreads-1 {
|
||||
end = numElements
|
||||
}
|
||||
|
||||
elements := make([]map[string]any, 0, end-start)
|
||||
for j := start; j < end; j++ {
|
||||
elements = append(elements, map[string]any{
|
||||
"id": j,
|
||||
"value": fmt.Sprintf("Value%d", j),
|
||||
})
|
||||
}
|
||||
encoded, err := json.Marshal(elements)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var parsed []map[string]any
|
||||
err = json.Unmarshal(encoded, &parsed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 内存性能
|
||||
|
||||
func (s *Service) memoryTestTask() map[string]any {
|
||||
results := make(map[string]any)
|
||||
dataSize := 500 * 1024 * 1024 // 500 MB
|
||||
data := make([]byte, dataSize)
|
||||
_, _ = cryptorand.Read(data)
|
||||
|
||||
start := time.Now()
|
||||
// 内存读写速度
|
||||
results["bandwidth"] = s.memoryBandwidthTest(data)
|
||||
// 内存访问延迟
|
||||
data = data[:100*1024*1024] // 100 MB
|
||||
results["latency"] = s.memoryLatencyTest(data)
|
||||
duration := time.Since(start)
|
||||
results["score"] = s.calculateScore(duration)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s *Service) memoryBandwidthTest(data []byte) string {
|
||||
dataSize := len(data)
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
for i := 0; i < dataSize; i++ {
|
||||
data[i] ^= 0xFF
|
||||
}
|
||||
|
||||
duration := time.Since(startTime).Seconds()
|
||||
if duration == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
speed := float64(dataSize) / duration / (1024 * 1024)
|
||||
return fmt.Sprintf("%.2f MB/s", speed)
|
||||
}
|
||||
|
||||
func (s *Service) memoryLatencyTest(data []byte) string {
|
||||
dataSize := len(data)
|
||||
indices := rand.Perm(dataSize)
|
||||
|
||||
startTime := time.Now()
|
||||
sum := byte(0)
|
||||
for _, idx := range indices {
|
||||
sum ^= data[idx]
|
||||
}
|
||||
duration := time.Since(startTime).Seconds()
|
||||
if duration == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
avgLatency := duration * 1e9 / float64(dataSize)
|
||||
return fmt.Sprintf("%.2f ns", avgLatency)
|
||||
}
|
||||
|
||||
// 硬盘IO
|
||||
|
||||
func (s *Service) diskTestTask() map[string]any {
|
||||
results := make(map[string]any)
|
||||
blockSizes := []int64{4 * 1024, 64 * 1024, 512 * 1024, 1 * 1024 * 1024} // 4K, 64K, 512K, 1M
|
||||
fileSize := int64(100 * 1024 * 1024) // 100MB 文件
|
||||
|
||||
start := time.Now()
|
||||
for _, blockSize := range blockSizes {
|
||||
result := s.diskIOTest(blockSize, fileSize)
|
||||
results[fmt.Sprintf("%d", blockSize/1024)] = result
|
||||
}
|
||||
duration := time.Since(start)
|
||||
results["score"] = s.calculateScore(duration)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s *Service) diskIOTest(blockSize int64, fileSize int64) map[string]any {
|
||||
result := make(map[string]any)
|
||||
tempFile := fmt.Sprintf("tempfile_%d", blockSize)
|
||||
defer os.Remove(tempFile)
|
||||
|
||||
// 写测试
|
||||
writeSpeed, writeIOPS := s.diskWriteTest(tempFile, blockSize, fileSize)
|
||||
// 读测试
|
||||
readSpeed, readIOPS := s.diskReadTest(tempFile, blockSize, fileSize)
|
||||
|
||||
result["write_speed"] = fmt.Sprintf("%.2f MB/s", writeSpeed)
|
||||
result["write_iops"] = fmt.Sprintf("%.2f IOPS", writeIOPS)
|
||||
result["read_speed"] = fmt.Sprintf("%.2f MB/s", readSpeed)
|
||||
result["read_iops"] = fmt.Sprintf("%.2f IOPS", readIOPS)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) diskWriteTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
|
||||
totalBlocks := fileSize / blockSize
|
||||
|
||||
data := make([]byte, blockSize)
|
||||
_, _ = cryptorand.Read(data)
|
||||
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_SYNC, 0644)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for i := int64(0); i < totalBlocks; i++ {
|
||||
// 生成随机偏移
|
||||
offset := rand.Int64N(fileSize - blockSize + 1)
|
||||
_, err := file.WriteAt(data, offset)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
_ = file.Sync()
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
if duration == 0 {
|
||||
duration = 1
|
||||
}
|
||||
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
|
||||
iops := float64(totalBlocks) / duration
|
||||
return speed, iops
|
||||
}
|
||||
|
||||
func (s *Service) diskReadTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
|
||||
totalBlocks := fileSize / blockSize
|
||||
|
||||
data := make([]byte, blockSize)
|
||||
|
||||
file, err := os.OpenFile(fileName, os.O_RDONLY|os.O_SYNC, 0644)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for i := int64(0); i < totalBlocks; i++ {
|
||||
// 生成随机偏移
|
||||
offset := rand.Int64N(fileSize - blockSize + 1)
|
||||
_, err := file.ReadAt(data, offset)
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
if duration == 0 {
|
||||
duration = 1
|
||||
}
|
||||
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
|
||||
iops := float64(totalBlocks) / duration
|
||||
return speed, iops
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package apps
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
_ "github.com/TheTNB/panel/internal/apps/benchmark"
|
||||
_ "github.com/TheTNB/panel/internal/apps/fail2ban"
|
||||
_ "github.com/TheTNB/panel/internal/apps/frp"
|
||||
_ "github.com/TheTNB/panel/internal/apps/gitea"
|
||||
|
||||
@@ -5,5 +5,5 @@ type UpdateConfig struct {
|
||||
}
|
||||
|
||||
type SetRootPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ func (s *Service) getExtensions() []Extension {
|
||||
{
|
||||
Name: "exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Description: "通过 exif 扩展,您可以操作图像元数据。",
|
||||
},
|
||||
{
|
||||
Name: "pdo_pgsql",
|
||||
|
||||
@@ -78,8 +78,10 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Port: req.Port,
|
||||
Protocol: "tcp",
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -2,7 +2,7 @@ package pureftpd
|
||||
|
||||
type Create struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Delete struct {
|
||||
|
||||
type ChangePassword struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
|
||||
@@ -156,8 +156,10 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Port: req.Port,
|
||||
Protocol: "tcp",
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -18,6 +18,10 @@ func init() {
|
||||
r.Post("/swap", service.UpdateSWAP)
|
||||
r.Get("/timezone", service.GetTimezone)
|
||||
r.Post("/timezone", service.UpdateTimezone)
|
||||
r.Post("/time", service.UpdateTime)
|
||||
r.Post("/syncTime", service.SyncTime)
|
||||
r.Get("/hostname", service.GetHostname)
|
||||
r.Post("/hostname", service.UpdateHostname)
|
||||
r.Get("/hosts", service.GetHosts)
|
||||
r.Post("/hosts", service.UpdateHosts)
|
||||
r.Post("/rootPassword", service.UpdateRootPassword)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package toolbox
|
||||
|
||||
import "time"
|
||||
|
||||
type DNS struct {
|
||||
DNS1 string `form:"dns1" json:"dns1" validate:"required"`
|
||||
DNS2 string `form:"dns2" json:"dns2" validate:"required"`
|
||||
@@ -13,10 +15,18 @@ type Timezone struct {
|
||||
Timezone string `form:"timezone" json:"timezone" validate:"required"`
|
||||
}
|
||||
|
||||
type Time struct {
|
||||
Time time.Time `form:"time" json:"time" validate:"required"`
|
||||
}
|
||||
|
||||
type Hostname struct {
|
||||
Hostname string `form:"hostname" json:"hostname" validate:"required,regex=^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$"`
|
||||
}
|
||||
|
||||
type Hosts struct {
|
||||
Hosts string `form:"hosts" json:"hosts"`
|
||||
}
|
||||
|
||||
type Password struct {
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/ntp"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
@@ -221,6 +222,64 @@ func (s *Service) UpdateTimezone(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateTime 设置时间
|
||||
func (s *Service) UpdateTime(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Time](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ntp.UpdateSystemTime(req.Time); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
|
||||
}
|
||||
|
||||
// SyncTime 同步时间
|
||||
func (s *Service) SyncTime(w http.ResponseWriter, r *http.Request) {
|
||||
now, err := ntp.Now()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ntp.UpdateSystemTime(now); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetHostname 获取主机名
|
||||
func (s *Service) GetHostname(w http.ResponseWriter, r *http.Request) {
|
||||
hostname, _ := io.Read("/etc/hostname")
|
||||
service.Success(w, strings.TrimSpace(hostname))
|
||||
}
|
||||
|
||||
// UpdateHostname 设置主机名
|
||||
func (s *Service) UpdateHostname(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Hostname](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("hostnamectl set-hostname '%s'", req.Hostname); err != nil {
|
||||
// 直接写 /etc/hostname
|
||||
if err = io.Write("/etc/hostname", req.Hostname, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "写入主机名失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetHosts 获取 hosts 信息
|
||||
func (s *Service) GetHosts(w http.ResponseWriter, r *http.Request) {
|
||||
hosts, _ := io.Read("/etc/hosts")
|
||||
@@ -251,11 +310,6 @@ func (s *Service) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(req.Password) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符")
|
||||
return
|
||||
}
|
||||
|
||||
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
|
||||
if _, err = shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -4,14 +4,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Info types.CurrentInfo `gorm:"not null;serializer:json" json:"info"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type MonitorRepo interface {
|
||||
|
||||
@@ -21,6 +21,7 @@ const (
|
||||
SettingKeySshPort SettingKey = "ssh_port"
|
||||
SettingKeySshUser SettingKey = "ssh_user"
|
||||
SettingKeySshPassword SettingKey = "ssh_password"
|
||||
SettingKeyOfflineMode SettingKey = "offline_mode"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
@@ -33,9 +34,11 @@ type Setting struct {
|
||||
|
||||
type SettingRepo interface {
|
||||
Get(key SettingKey, defaultValue ...string) (string, error)
|
||||
GetBool(key SettingKey, defaultValue ...bool) (bool, error)
|
||||
Set(key SettingKey, value string) error
|
||||
Delete(key SettingKey) error
|
||||
GetPanelSetting(ctx context.Context) (*request.PanelSetting, error)
|
||||
UpdatePanelSetting(ctx context.Context, setting *request.PanelSetting) (bool, error)
|
||||
UpdatePanel(version, url, checksum string) error
|
||||
FixPanel() error
|
||||
}
|
||||
|
||||
@@ -28,5 +28,5 @@ type TaskRepo interface {
|
||||
Delete(id uint) error
|
||||
UpdateStatus(id uint, status TaskStatus) error
|
||||
Push(task *Task) error
|
||||
DispatchWaiting() error
|
||||
DispatchWaiting()
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ func BootWeb() {
|
||||
boot()
|
||||
initValidator()
|
||||
initSession()
|
||||
initCron()
|
||||
initQueue()
|
||||
initCron()
|
||||
initHttp()
|
||||
|
||||
select {}
|
||||
|
||||
@@ -2,6 +2,7 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -13,6 +14,8 @@ import (
|
||||
)
|
||||
|
||||
func initCli() {
|
||||
app.IsCli = true
|
||||
|
||||
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", "名称")
|
||||
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", "用法")
|
||||
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", "版本")
|
||||
@@ -43,6 +46,6 @@ func initCli() {
|
||||
Commands: route.Cli(),
|
||||
}
|
||||
if err := cmd.Run(context.Background(), os.Args); err != nil {
|
||||
color.Redln(err.Error())
|
||||
color.Redln(fmt.Sprintf("|-%v", err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func initConf() {
|
||||
|
||||
func initGlobal() {
|
||||
app.Root = app.Conf.MustString("app.root")
|
||||
app.Version = "2.3.4"
|
||||
app.Version = "2.3.10"
|
||||
app.Locale = app.Conf.MustString("app.locale")
|
||||
|
||||
// 初始化时区
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/go-playground/validator/v10/translations/zh"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/http/rule"
|
||||
)
|
||||
|
||||
func initValidator() {
|
||||
@@ -23,4 +24,7 @@ func initValidator() {
|
||||
|
||||
app.Translator = &trans
|
||||
app.Validator = validate
|
||||
if err := rule.RegisterRules(validate); err != nil {
|
||||
log.Fatalf("failed to register validator rules: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
)
|
||||
|
||||
@@ -179,16 +180,17 @@ func (r *appRepo) Install(channel, slug string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = "安装应用 " + item.Name
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err = r.taskRepo.Push(task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
return r.taskRepo.Push(task)
|
||||
}
|
||||
|
||||
func (r *appRepo) UnInstall(slug string) error {
|
||||
@@ -233,16 +235,17 @@ func (r *appRepo) UnInstall(slug string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = "卸载应用 " + item.Name
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err = r.taskRepo.Push(task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
return r.taskRepo.Push(task)
|
||||
}
|
||||
|
||||
func (r *appRepo) Update(slug string) error {
|
||||
@@ -287,16 +290,17 @@ func (r *appRepo) Update(slug string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = "更新应用 " + item.Name
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err = r.taskRepo.Push(task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
return r.taskRepo.Push(task)
|
||||
}
|
||||
|
||||
func (r *appRepo) UpdateShow(slug string, show bool) error {
|
||||
|
||||
@@ -55,6 +55,7 @@ func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
|
||||
Name: file.Name(),
|
||||
Path: filepath.Join(path, file.Name()),
|
||||
Size: str.FormatBytes(float64(info.Size())),
|
||||
Time: info.ModTime(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -180,9 +181,15 @@ func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
|
||||
toDelete := filtered[save:]
|
||||
for _, file := range toDelete {
|
||||
filePath := filepath.Join(path, file.Name())
|
||||
color.Greenln(fmt.Sprintf("|-清理过期文件:%s", filePath))
|
||||
if app.IsCli {
|
||||
color.Greenln(fmt.Sprintf("|-清理过期文件:%s", filePath))
|
||||
}
|
||||
if err = os.Remove(filePath); err != nil {
|
||||
color.Redln(fmt.Sprintf("|-清理失败:%v", err))
|
||||
if app.IsCli {
|
||||
color.Redln(fmt.Sprintf("|-清理失败:%v", err))
|
||||
} else {
|
||||
return fmt.Errorf("清理失败:%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,8 +242,10 @@ func (r *backupRepo) createWebsite(to string, name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
|
||||
if app.IsCli {
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -280,8 +289,10 @@ func (r *backupRepo) createMySQL(to string, name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
|
||||
if app.IsCli {
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -315,8 +326,10 @@ func (r *backupRepo) createPostgres(to string, name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
|
||||
if app.IsCli {
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -336,9 +349,14 @@ func (r *backupRepo) createPanel(to string) error {
|
||||
}, backup, io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := io.Chmod(backup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
|
||||
if app.IsCli {
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -459,10 +477,12 @@ func (r *backupRepo) preCheckPath(to, path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
if app.IsCli {
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
}
|
||||
|
||||
if uint64(size) > usage.Free {
|
||||
return errors.New("备份目录空间不足")
|
||||
@@ -483,9 +503,11 @@ func (r *backupRepo) preCheckDB(to string, size int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
if app.IsCli {
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
}
|
||||
|
||||
if uint64(size) > usage.Free {
|
||||
return errors.New("备份目录空间不足")
|
||||
|
||||
@@ -93,11 +93,11 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, cert.Website.Path)
|
||||
client.UseHTTP(conf)
|
||||
}
|
||||
}
|
||||
|
||||
ssl, err := client.ObtainSSL(context.Background(), cert.Domains, acme.KeyType(cert.Type))
|
||||
ssl, err := client.ObtainCertificate(context.Background(), cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
|
||||
return nil, errors.New("请重新获取 DNS 解析记录")
|
||||
}
|
||||
|
||||
ssl, err := r.client.ObtainSSLManual()
|
||||
ssl, err := r.client.ObtainCertificateManual()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -172,11 +172,11 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, cert.Website.Path)
|
||||
client.UseHTTP(conf)
|
||||
}
|
||||
}
|
||||
|
||||
ssl, err := client.RenewSSL(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
|
||||
ssl, err := client.RenewCertificate(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,10 +236,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil {
|
||||
if err = io.Write(fmt.Sprintf("%s/server/vhost/cert/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil {
|
||||
if err = io.Write(fmt.Sprintf("%s/server/vhost/cert/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
@@ -251,6 +251,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
}
|
||||
|
||||
func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) {
|
||||
if cert.Account == nil {
|
||||
return nil, errors.New("该证书没有关联账号,无法签发")
|
||||
}
|
||||
|
||||
var ca string
|
||||
var eab *acme.EAB
|
||||
switch cert.Account.CA {
|
||||
|
||||
@@ -50,7 +50,7 @@ func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error {
|
||||
}
|
||||
|
||||
func (r monitorRepo) Clear() error {
|
||||
return app.Orm.Delete(&biz.Monitor{}).Error
|
||||
return app.Orm.Where("1 = 1").Delete(&biz.Monitor{}).Error
|
||||
}
|
||||
|
||||
func (r monitorRepo) List(start, end time.Time) ([]*biz.Monitor, error) {
|
||||
|
||||
@@ -60,7 +60,7 @@ func (r *safeRepo) UpdateSSH(port uint, status bool) error {
|
||||
}
|
||||
|
||||
func (r *safeRepo) GetPingStatus() (bool, error) {
|
||||
out, err := shell.Execf(`firewall-cmd --list-all`)
|
||||
out, err := shell.Execf(`firewall-cmd --list-rich-rules`)
|
||||
if err != nil {
|
||||
return true, errors.New(out)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"slices"
|
||||
|
||||
"github.com/go-rat/utils/hash"
|
||||
"github.com/goccy/go-yaml"
|
||||
@@ -16,16 +16,21 @@ import (
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/firewall"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type settingRepo struct{}
|
||||
type settingRepo struct {
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewSettingRepo() biz.SettingRepo {
|
||||
return &settingRepo{}
|
||||
return &settingRepo{
|
||||
taskRepo: NewTaskRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) {
|
||||
@@ -43,6 +48,21 @@ func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, e
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (r *settingRepo) GetBool(key biz.SettingKey, defaultValue ...bool) (bool, error) {
|
||||
setting := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if setting.Value == "" && len(defaultValue) > 0 {
|
||||
return defaultValue[0], nil
|
||||
}
|
||||
|
||||
return cast.ToBool(setting.Value), nil
|
||||
}
|
||||
|
||||
func (r *settingRepo) Set(key biz.SettingKey, value string) error {
|
||||
setting := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
|
||||
@@ -66,16 +86,20 @@ func (r *settingRepo) Delete(key biz.SettingKey) error {
|
||||
}
|
||||
|
||||
func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSetting, error) {
|
||||
name := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", biz.SettingKeyName).First(name).Error; err != nil {
|
||||
name, err := r.Get(biz.SettingKeyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websitePath := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", biz.SettingKeyWebsitePath).First(websitePath).Error; err != nil {
|
||||
offlineMode, err := r.Get(biz.SettingKeyOfflineMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backupPath := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", biz.SettingKeyBackupPath).First(backupPath).Error; err != nil {
|
||||
websitePath, err := r.Get(biz.SettingKeyWebsitePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backupPath, err := r.Get(biz.SettingKeyBackupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -95,11 +119,12 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
|
||||
}
|
||||
|
||||
return &request.PanelSetting{
|
||||
Name: name.Value,
|
||||
Name: name,
|
||||
Locale: app.Conf.String("app.locale"),
|
||||
Entrance: app.Conf.String("http.entrance"),
|
||||
WebsitePath: websitePath.Value,
|
||||
BackupPath: backupPath.Value,
|
||||
OfflineMode: cast.ToBool(offlineMode),
|
||||
WebsitePath: websitePath,
|
||||
BackupPath: backupPath,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Port: app.Conf.Int("http.port"),
|
||||
@@ -113,6 +138,9 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
if err := r.Set(biz.SettingKeyName, setting.Name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyOfflineMode, cast.ToString(setting.OfflineMode)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyWebsitePath, setting.WebsitePath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -120,6 +148,37 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 用户
|
||||
user := new(biz.User)
|
||||
userID := cast.ToUint(ctx.Value("user_id"))
|
||||
if err := app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
user.Username = setting.Username
|
||||
user.Email = setting.Email
|
||||
if setting.Password != "" {
|
||||
value, err := hash.NewArgon2id().Make(setting.Password)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
user.Password = value
|
||||
}
|
||||
if err := app.Orm.Save(user).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 下面是需要需要重启的设置
|
||||
// 面板HTTPS
|
||||
restartFlag := false
|
||||
oldCert, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key"))
|
||||
if oldCert != setting.Cert || oldKey != setting.Key {
|
||||
if r.taskRepo.HasRunningTask() {
|
||||
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
|
||||
}
|
||||
restartFlag = true
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -127,7 +186,7 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
return false, err
|
||||
}
|
||||
|
||||
restartFlag := false
|
||||
// 面板主配置
|
||||
config := new(types.PanelConfig)
|
||||
cm := yaml.CommentMap{}
|
||||
raw, err := io.Read("/usr/local/etc/panel/config.yml")
|
||||
@@ -143,33 +202,29 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
config.HTTP.Entrance = setting.Entrance
|
||||
config.HTTP.TLS = setting.HTTPS
|
||||
|
||||
// 放行端口
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
PortStart: uint(config.HTTP.Port),
|
||||
PortEnd: uint(config.HTTP.Port),
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
encoded, err := yaml.MarshalWithOptions(config, yaml.WithComment(cm))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if raw != string(encoded) {
|
||||
if r.taskRepo.HasRunningTask() {
|
||||
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
|
||||
}
|
||||
restartFlag = true
|
||||
}
|
||||
|
||||
user := new(biz.User)
|
||||
userID := cast.ToUint(ctx.Value("user_id"))
|
||||
if err = app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
user.Username = setting.Username
|
||||
user.Email = setting.Email
|
||||
if setting.Password != "" {
|
||||
value, err := hash.NewArgon2id().Make(setting.Password)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
user.Password = value
|
||||
}
|
||||
if err = app.Orm.Save(user).Error; err != nil {
|
||||
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -186,107 +241,119 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
||||
}
|
||||
|
||||
name := filepath.Base(url)
|
||||
color.Greenln(fmt.Sprintf("目标版本:%s", version))
|
||||
color.Greenln(fmt.Sprintf("下载链接:%s", url))
|
||||
color.Greenln(fmt.Sprintf("文件名:%s", name))
|
||||
|
||||
color.Greenln("前置检查...")
|
||||
if io.Exists("/tmp/panel-storage.zip") {
|
||||
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败导致的,请排除后重试")
|
||||
if app.IsCli {
|
||||
color.Greenln(fmt.Sprintf("|-目标版本:%s", version))
|
||||
color.Greenln(fmt.Sprintf("|-下载链接:%s", url))
|
||||
color.Greenln(fmt.Sprintf("|-文件名:%s", name))
|
||||
}
|
||||
|
||||
color.Greenln("备份面板数据...")
|
||||
if app.IsCli {
|
||||
color.Greenln("|-正在下载...")
|
||||
}
|
||||
if _, err := shell.Execf("wget -T 120 -t 3 -O /tmp/%s %s", name, url); err != nil {
|
||||
return fmt.Errorf("下载失败:%w", err)
|
||||
}
|
||||
if _, err := shell.Execf("wget -T 20 -t 3 -O /tmp/%s %s", name+".sha256", checksum); err != nil {
|
||||
return fmt.Errorf("下载失败:%w", err)
|
||||
}
|
||||
if !io.Exists(filepath.Join("/tmp", name)) || !io.Exists(filepath.Join("/tmp", name+".sha256")) {
|
||||
return errors.New("下载文件检查失败")
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
color.Greenln("|-校验下载文件...")
|
||||
}
|
||||
if check, err := shell.Execf("cd /tmp && sha256sum -c %s --ignore-missing", name+".sha256"); check != name+": OK" || err != nil {
|
||||
return errors.New("下载文件校验失败")
|
||||
}
|
||||
if err := io.Remove(filepath.Join("/tmp", name+".sha256")); err != nil {
|
||||
if app.IsCli {
|
||||
color.Redln("|-清理校验文件失败:", err)
|
||||
}
|
||||
return fmt.Errorf("清理校验文件失败:%w", err)
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
color.Greenln("|-前置检查...")
|
||||
}
|
||||
if io.Exists("/tmp/panel-storage.zip") {
|
||||
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败所致,请运行 panel-cli fix 修复后重试")
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
color.Greenln("|-备份面板数据...")
|
||||
}
|
||||
// 备份面板
|
||||
if err := io.Compress([]string{filepath.Join(app.Root, "panel")}, filepath.Join(app.Root, fmt.Sprintf("backup/panel/panel-%s.zip", time.Now().Format("20060102150405"))), io.Zip); err != nil {
|
||||
color.Redln("备份面板失败:", err)
|
||||
return err
|
||||
backup := NewBackupRepo()
|
||||
if err := backup.Create(biz.BackupTypePanel, ""); err != nil {
|
||||
if app.IsCli {
|
||||
color.Redln("|-备份面板失败:", err)
|
||||
}
|
||||
return fmt.Errorf("备份面板失败:%w", err)
|
||||
}
|
||||
if err := io.Compress([]string{filepath.Join(app.Root, "panel/storage")}, "/tmp/panel-storage.zip", io.Zip); err != nil {
|
||||
color.Redln("备份面板数据失败:", err)
|
||||
return err
|
||||
if app.IsCli {
|
||||
color.Redln("|-备份面板数据失败:", err)
|
||||
}
|
||||
return fmt.Errorf("备份面板数据失败:%w", err)
|
||||
}
|
||||
if !io.Exists("/tmp/panel-storage.zip") {
|
||||
return errors.New("已备份面板数据检查失败")
|
||||
}
|
||||
color.Greenln("备份完成")
|
||||
|
||||
color.Greenln("清理旧版本...")
|
||||
if app.IsCli {
|
||||
color.Greenln("|-清理旧版本...")
|
||||
}
|
||||
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
|
||||
color.Redln("清理旧版本失败:", err)
|
||||
return err
|
||||
return fmt.Errorf("清理旧版本失败:%w", err)
|
||||
}
|
||||
color.Greenln("清理完成")
|
||||
|
||||
color.Greenln("正在下载...")
|
||||
if _, err := shell.Execf("wget -T 120 -t 3 -O %s/panel/%s %s", app.Root, name, url); err != nil {
|
||||
color.Redln("下载失败:", err)
|
||||
return err
|
||||
if app.IsCli {
|
||||
color.Greenln("|-解压新版本...")
|
||||
}
|
||||
if _, err := shell.Execf("wget -T 20 -t 3 -O %s/panel/%s %s", app.Root, name+".sha256", checksum); err != nil {
|
||||
color.Redln("下载失败:", err)
|
||||
return err
|
||||
}
|
||||
if !io.Exists(filepath.Join(app.Root, "panel", name)) || !io.Exists(filepath.Join(app.Root, "panel", name+".sha256")) {
|
||||
return errors.New("下载文件检查失败")
|
||||
}
|
||||
color.Greenln("下载完成")
|
||||
|
||||
color.Greenln("校验下载文件...")
|
||||
check, err := shell.Execf("cd %s/panel && sha256sum -c %s --ignore-missing", app.Root, name+".sha256")
|
||||
if check != name+": OK" || err != nil {
|
||||
return errors.New("下载文件校验失败")
|
||||
}
|
||||
if err = io.Remove(filepath.Join(app.Root, "panel", name+".sha256")); err != nil {
|
||||
color.Redln("清理校验文件失败:", err)
|
||||
return err
|
||||
}
|
||||
color.Greenln("文件校验完成")
|
||||
|
||||
color.Greenln("更新新版本...")
|
||||
if _, err = shell.Execf("cd %s/panel && unzip -o %s && rm -rf %s", app.Root, name, name); err != nil {
|
||||
color.Redln("更新失败:", err)
|
||||
return err
|
||||
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel"), io.Zip); err != nil {
|
||||
return fmt.Errorf("解压失败:%w", err)
|
||||
}
|
||||
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
|
||||
return errors.New("更新失败,可能是下载过程中出现了问题")
|
||||
return errors.New("解压失败,缺失文件")
|
||||
}
|
||||
color.Greenln("更新完成")
|
||||
|
||||
color.Greenln("恢复面板数据...")
|
||||
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
|
||||
color.Redln("恢复面板数据失败:", err)
|
||||
return err
|
||||
if app.IsCli {
|
||||
color.Greenln("|-恢复面板数据...")
|
||||
}
|
||||
if err := io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
|
||||
return fmt.Errorf("恢复面板数据失败:%w", err)
|
||||
}
|
||||
if !io.Exists(filepath.Join(app.Root, "panel/storage/app.db")) {
|
||||
return errors.New("恢复面板数据失败")
|
||||
}
|
||||
color.Greenln("恢复完成")
|
||||
|
||||
color.Greenln("运行升级后脚本...")
|
||||
if _, err = shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
|
||||
color.Redln("运行面板升级后脚本失败:", err)
|
||||
return err
|
||||
if app.IsCli {
|
||||
color.Greenln("|-运行更新后脚本...")
|
||||
}
|
||||
if _, err = shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
|
||||
color.Redln("下载面板服务文件失败:", err)
|
||||
return err
|
||||
if _, err := shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
|
||||
return fmt.Errorf("运行面板更新后脚本失败:%w", err)
|
||||
}
|
||||
if _, err = shell.Execf("panel-cli setting write version %s", version); err != nil {
|
||||
color.Redln("写入面板版本号失败:", err)
|
||||
return err
|
||||
if _, err := shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
|
||||
return fmt.Errorf("下载面板服务文件失败:%w", err)
|
||||
}
|
||||
if err = io.Mv(filepath.Join(app.Root, "panel/cli"), "/usr/local/sbin/panel-cli"); err != nil {
|
||||
color.Redln("移动面板命令行工具失败:", err)
|
||||
return err
|
||||
if _, err := shell.Execf("panel-cli setting write version %s", version); err != nil {
|
||||
return fmt.Errorf("写入面板版本号失败:%w", err)
|
||||
}
|
||||
if err := io.Mv(filepath.Join(app.Root, "panel/cli"), "/usr/local/sbin/panel-cli"); err != nil {
|
||||
return fmt.Errorf("移动面板命令行工具失败:%w", err)
|
||||
}
|
||||
|
||||
color.Greenln("设置面板文件权限...")
|
||||
if app.IsCli {
|
||||
color.Greenln("|-设置关键文件权限...")
|
||||
}
|
||||
_ = io.Chmod("/usr/local/sbin/panel-cli", 0700)
|
||||
_ = io.Chmod("/etc/systemd/system/panel.service", 0700)
|
||||
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
|
||||
color.Greenln("设置完成")
|
||||
|
||||
color.Greenln("升级完成")
|
||||
if app.IsCli {
|
||||
color.Greenln("|-更新完成")
|
||||
}
|
||||
|
||||
_, _ = shell.Execf("systemctl daemon-reload")
|
||||
_ = io.Remove("/tmp/panel-storage.zip")
|
||||
@@ -295,3 +362,134 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *settingRepo) FixPanel() error {
|
||||
if app.IsCli {
|
||||
color.Greenln("|-开始修复面板...")
|
||||
}
|
||||
// 检查关键文件是否正常
|
||||
flag := false
|
||||
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
|
||||
flag = true
|
||||
}
|
||||
if !io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) {
|
||||
flag = true
|
||||
}
|
||||
if io.Exists("/tmp/panel-storage.zip") {
|
||||
flag = true
|
||||
}
|
||||
if !flag {
|
||||
return fmt.Errorf("文件正常无需修复,请运行 panel-cli update 更新面板")
|
||||
}
|
||||
|
||||
// 再次确认是否需要修复
|
||||
if io.Exists("/tmp/panel-storage.zip") {
|
||||
// 文件齐全情况下只移除临时文件
|
||||
if io.Exists(filepath.Join(app.Root, "panel", "web")) &&
|
||||
io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) &&
|
||||
io.Exists("/usr/local/etc/panel/config.yml") {
|
||||
if err := io.Remove("/tmp/panel-storage.zip"); err != nil {
|
||||
return fmt.Errorf("清理临时文件失败:%w", err)
|
||||
}
|
||||
if app.IsCli {
|
||||
color.Greenln("已清理临时文件,请运行 panel-cli update 更新面板")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 从备份目录中找最新的备份文件
|
||||
backup := NewBackupRepo()
|
||||
list, err := backup.List(biz.BackupTypePanel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slices.SortFunc(list, func(a *types.BackupFile, b *types.BackupFile) int {
|
||||
return int(b.Time.Unix() - a.Time.Unix())
|
||||
})
|
||||
if len(list) == 0 {
|
||||
return fmt.Errorf("未找到备份文件,无法自动修复")
|
||||
}
|
||||
latest := list[0]
|
||||
if app.IsCli {
|
||||
color.Greenln(fmt.Sprintf("|-使用备份文件:%s", latest.Name))
|
||||
}
|
||||
|
||||
// 解压备份文件
|
||||
if app.IsCli {
|
||||
color.Greenln("|-解压备份文件...")
|
||||
}
|
||||
if err = io.Remove("/tmp/panel-fix"); err != nil {
|
||||
return fmt.Errorf("清理临时目录失败:%w", err)
|
||||
}
|
||||
if err = io.UnCompress(latest.Path, "/tmp/panel-fix", io.Zip); err != nil {
|
||||
return fmt.Errorf("解压备份文件失败:%w", err)
|
||||
}
|
||||
|
||||
// 移动文件到对应位置
|
||||
if app.IsCli {
|
||||
color.Greenln("|-移动备份文件...")
|
||||
}
|
||||
if io.Exists(filepath.Join("/tmp/panel-fix", "panel")) && io.IsDir(filepath.Join("/tmp/panel-fix", "panel")) {
|
||||
if err = io.Remove(filepath.Join(app.Root, "panel")); err != nil {
|
||||
return fmt.Errorf("删除目录失败:%w", err)
|
||||
}
|
||||
if err = io.Mv(filepath.Join("/tmp/panel-fix", "panel"), filepath.Join(app.Root)); err != nil {
|
||||
return fmt.Errorf("移动目录失败:%w", err)
|
||||
}
|
||||
}
|
||||
if io.Exists(filepath.Join("/tmp/panel-fix", "config.yml")) {
|
||||
if err = io.Mv(filepath.Join("/tmp/panel-fix", "config.yml"), "/usr/local/etc/panel/config.yml"); err != nil {
|
||||
return fmt.Errorf("移动文件失败:%w", err)
|
||||
}
|
||||
}
|
||||
if io.Exists(filepath.Join("/tmp/panel-fix", "panel-cli")) {
|
||||
if err = io.Mv(filepath.Join("/tmp/panel-fix", "panel-cli"), "/usr/local/sbin/panel-cli"); err != nil {
|
||||
return fmt.Errorf("移动文件失败:%w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// tmp目录下如果有storage备份,则解压回去
|
||||
if app.IsCli {
|
||||
color.Greenln("|-恢复面板数据...")
|
||||
}
|
||||
if io.Exists("/tmp/panel-storage.zip") {
|
||||
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
|
||||
return fmt.Errorf("恢复面板数据失败:%w", err)
|
||||
}
|
||||
if err = io.Remove("/tmp/panel-storage.zip"); err != nil {
|
||||
return fmt.Errorf("清理临时文件失败:%w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 下载服务文件
|
||||
if !io.Exists("/etc/systemd/system/panel.service") {
|
||||
if _, err = shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 处理权限
|
||||
if app.IsCli {
|
||||
color.Greenln("|-设置关键文件权限...")
|
||||
}
|
||||
if err = io.Chmod("/usr/local/etc/panel/config.yml", 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Chmod("/etc/systemd/system/panel.service", 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Chmod("/usr/local/sbin/panel-cli", 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Chmod(filepath.Join(app.Root, "panel"), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
color.Greenln("|-修复完成")
|
||||
}
|
||||
|
||||
tools.RestartPanel()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/queuejob"
|
||||
)
|
||||
|
||||
var taskDispatchOnce sync.Once
|
||||
|
||||
type taskRepo struct{}
|
||||
|
||||
func NewTaskRepo() biz.TaskRepo {
|
||||
task := &taskRepo{}
|
||||
taskDispatchOnce.Do(task.DispatchWaiting)
|
||||
return &taskRepo{}
|
||||
}
|
||||
|
||||
@@ -48,17 +56,24 @@ func (r *taskRepo) Push(task *biz.Task) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (r *taskRepo) DispatchWaiting() error {
|
||||
func (r *taskRepo) DispatchWaiting() {
|
||||
// cli下不处理
|
||||
if app.IsCli {
|
||||
return
|
||||
}
|
||||
|
||||
var tasks []biz.Task
|
||||
if err := app.Orm.Where("status = ?", biz.TaskStatusWaiting).Find(&tasks).Error; err != nil {
|
||||
return err
|
||||
app.Logger.Error("获取待处理任务失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
if err := r.Push(&task); err != nil {
|
||||
return err
|
||||
if err := app.Queue.Push(queuejob.NewProcessTask(r), []any{
|
||||
task.ID,
|
||||
}); err != nil {
|
||||
app.Logger.Error("推送任务失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -131,9 +131,9 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
||||
setting.OCSP = p.GetOCSP()
|
||||
}
|
||||
// 证书
|
||||
crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"))
|
||||
crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem"))
|
||||
setting.SSLCertificate = crt
|
||||
key, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"))
|
||||
key, _ := io.Read(filepath.Join(app.Root, "server/vhost/cert", website.Name+".key"))
|
||||
setting.SSLCertificateKey = key
|
||||
// 解析证书信息
|
||||
if decode, err := cert.ParseCert(crt); err == nil {
|
||||
@@ -208,13 +208,15 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
|
||||
if err = p.SetPHP(req.PHP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 伪静态
|
||||
// 伪静态和acme
|
||||
includes, comments, err := p.GetIncludes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"))
|
||||
includes = append(includes, filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"))
|
||||
comments = append(comments, []string{"# 伪静态规则"})
|
||||
comments = append(comments, []string{"# acme http-01"})
|
||||
if err = p.SetIncludes(includes, comments); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -252,10 +254,13 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"), "", 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".pem"), "", 0644); err != nil {
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"), "", 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".key"), "", 0644); err != nil {
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/cert", req.Name+".pem"), "", 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/cert", req.Name+".key"), "", 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -389,8 +394,8 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
|
||||
return err
|
||||
}
|
||||
// HTTPS
|
||||
certPath := filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem")
|
||||
keyPath := filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key")
|
||||
certPath := filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem")
|
||||
keyPath := filepath.Join(app.Root, "server/vhost/cert", website.Name+".key")
|
||||
if err = io.Write(certPath, req.SSLCertificate, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -471,14 +476,14 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
||||
return err
|
||||
}
|
||||
if website.Cert != nil {
|
||||
return errors.New("网站" + website.Name + "已绑定SSL证书,请先删除证书")
|
||||
return errors.New("网站" + website.Name + "已绑定证书,请先删除证书")
|
||||
}
|
||||
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/cert", website.Name+".key"))
|
||||
|
||||
if req.Path {
|
||||
_ = io.Remove(website.Path)
|
||||
@@ -551,7 +556,9 @@ func (r *websiteRepo) ResetConfig(id uint) error {
|
||||
return err
|
||||
}
|
||||
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
|
||||
includes = append(includes, filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"))
|
||||
comments = append(comments, []string{"# 伪静态规则"})
|
||||
comments = append(comments, []string{"# acme http-01"})
|
||||
if err = p.SetIncludes(includes, comments); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -569,10 +576,13 @@ func (r *websiteRepo) ResetConfig(id uint) error {
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
website.Status = true
|
||||
website.Https = false
|
||||
if err := app.Orm.Save(website).Error; err != nil {
|
||||
if err = app.Orm.Save(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,35 +5,35 @@ import (
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
)
|
||||
|
||||
// Status 检查程序状态
|
||||
func Status(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch types.Status {
|
||||
case types.StatusUpgrade:
|
||||
switch app.Status {
|
||||
case app.StatusUpgrade:
|
||||
render := chix.NewRender(w)
|
||||
render.Status(http.StatusServiceUnavailable)
|
||||
render.JSON(chix.M{
|
||||
"message": "面板升级中,请稍后刷新",
|
||||
"message": "面板更新中,请稍后刷新",
|
||||
})
|
||||
return
|
||||
case types.StatusMaintain:
|
||||
case app.StatusMaintain:
|
||||
render := chix.NewRender(w)
|
||||
render.Status(http.StatusServiceUnavailable)
|
||||
render.JSON(chix.M{
|
||||
"message": "面板正在运行维护任务,请稍后刷新",
|
||||
})
|
||||
return
|
||||
case types.StatusClosed:
|
||||
case app.StatusClosed:
|
||||
render := chix.NewRender(w)
|
||||
render.Status(http.StatusForbidden)
|
||||
render.JSON(chix.M{
|
||||
"message": "面板已关闭",
|
||||
})
|
||||
return
|
||||
case types.StatusFailed:
|
||||
case app.StatusFailed:
|
||||
render := chix.NewRender(w)
|
||||
render.Status(http.StatusInternalServerError)
|
||||
render.JSON(chix.M{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package request
|
||||
|
||||
type App struct {
|
||||
Slug string `json:"slug" form:"slug" validate:"required"`
|
||||
Slug string `json:"slug" form:"slug" validate:"required,not_exists=apps slug"`
|
||||
Channel string `json:"channel" form:"channel" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ type AppSlug struct {
|
||||
}
|
||||
|
||||
type AppUpdateShow struct {
|
||||
Slug string `json:"slug" form:"slug" validate:"required"`
|
||||
Show bool `json:"show" form:"show" validate:"required"`
|
||||
Slug string `json:"slug" form:"slug" validate:"required,exists=apps slug"`
|
||||
Show bool `json:"show" form:"show"`
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type CertCreate struct {
|
||||
}
|
||||
|
||||
type CertUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=certs id"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||
@@ -20,6 +20,6 @@ type CertUpdate struct {
|
||||
}
|
||||
|
||||
type CertDeploy struct {
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=certs id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ type CertAccountCreate struct {
|
||||
}
|
||||
|
||||
type CertAccountUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=cert_accounts id"`
|
||||
CA string `form:"ca" json:"ca" validate:"required"`
|
||||
Email string `form:"email" json:"email" validate:"required"`
|
||||
Kid string `form:"kid" json:"kid"`
|
||||
|
||||
@@ -9,7 +9,7 @@ type CertDNSCreate struct {
|
||||
}
|
||||
|
||||
type CertDNSUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=cert_dns id"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Data acme.DNSParam `form:"data" json:"data"`
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package request
|
||||
|
||||
type CronCreate struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required,not_exists=crons name"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required,cron"`
|
||||
Script string `form:"script" json:"script"`
|
||||
BackupType string `form:"backup_type" json:"backup_type" validate:"required"`
|
||||
BackupPath string `form:"backup_path" json:"backup_path"`
|
||||
@@ -12,13 +12,13 @@ type CronCreate struct {
|
||||
}
|
||||
|
||||
type CronUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=crons id"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required,cron"`
|
||||
Script string `form:"script" json:"script" validate:"required"`
|
||||
}
|
||||
|
||||
type CronStatus struct {
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=crons id"`
|
||||
Status bool `form:"status" json:"status"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package request
|
||||
|
||||
type DashboardCurrent struct {
|
||||
Nets []string `json:"nets" form:"nets"`
|
||||
Disks []string `json:"disks" form:"disks"`
|
||||
}
|
||||
@@ -4,7 +4,27 @@ type FirewallStatus struct {
|
||||
Status bool `json:"status" form:"status"`
|
||||
}
|
||||
|
||||
type FirewallCreateRule struct {
|
||||
Port uint `json:"port" validate:"required"`
|
||||
Protocol string `json:"protocol" validate:"required"`
|
||||
type FirewallRule struct {
|
||||
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||
PortStart uint `json:"port_start" validate:"required,gte=1,lte=65535"`
|
||||
PortEnd uint `json:"port_end" validate:"required,gte=1,lte=65535"`
|
||||
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
|
||||
Address string `json:"address"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
|
||||
Direction string `json:"direction"`
|
||||
}
|
||||
|
||||
type FirewallIPRule struct {
|
||||
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
|
||||
Address string `json:"address"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
|
||||
Direction string `json:"direction"`
|
||||
}
|
||||
|
||||
type FirewallForward struct {
|
||||
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
|
||||
Port uint `json:"port" validate:"required,gte=1,lte=65535"`
|
||||
TargetIP string `json:"target_ip" validate:"required"`
|
||||
TargetPort uint `json:"target_port" validate:"required,gte=1,lte=65535"`
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ type PanelSetting struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Locale string `json:"locale" validate:"required"`
|
||||
Entrance string `json:"entrance" validate:"required"`
|
||||
OfflineMode bool `json:"offline_mode"`
|
||||
WebsitePath string `json:"website_path" validate:"required"`
|
||||
BackupPath string `json:"backup_path" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password"`
|
||||
Password string `json:"password" validate:"password"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
Port int `json:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
HTTPS bool `json:"https"`
|
||||
|
||||
@@ -8,7 +8,7 @@ type WebsiteDefaultConfig struct {
|
||||
}
|
||||
|
||||
type WebsiteCreate struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required,not_exists=websites name"`
|
||||
Listens []string `form:"listens" json:"listens" validate:"min=1,dive,required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||
Path string `form:"path" json:"path"`
|
||||
@@ -17,17 +17,17 @@ type WebsiteCreate struct {
|
||||
DBType string `form:"db_type" json:"db_type"`
|
||||
DBName string `form:"db_name" json:"db_name"`
|
||||
DBUser string `form:"db_user" json:"db_user"`
|
||||
DBPassword string `form:"db_password" json:"db_password"`
|
||||
DBPassword string `form:"db_password" json:"db_password" validate:"password"`
|
||||
}
|
||||
|
||||
type WebsiteDelete struct {
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=websites id"`
|
||||
Path bool `form:"path" json:"path"`
|
||||
DB bool `form:"db" json:"db"`
|
||||
}
|
||||
|
||||
type WebsiteUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=websites id"`
|
||||
Listens []types.WebsiteListen `form:"listens" json:"listens" validate:"min=1"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||
HTTPS bool `form:"https" json:"https"`
|
||||
@@ -46,11 +46,11 @@ type WebsiteUpdate struct {
|
||||
}
|
||||
|
||||
type WebsiteUpdateRemark struct {
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=websites id"`
|
||||
Remark string `form:"remark" json:"remark"`
|
||||
}
|
||||
|
||||
type WebsiteUpdateStatus struct {
|
||||
ID uint `json:"id" form:"id" validate:"required"`
|
||||
ID uint `json:"id" form:"id" validate:"required,exists=websites id"`
|
||||
Status bool `json:"status" form:"status"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Exists struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func NewExists(db *gorm.DB) *Exists {
|
||||
return &Exists{DB: db}
|
||||
}
|
||||
|
||||
// Exists 格式 `exists=categories id other_field`
|
||||
func (r *Exists) Exists(fl validator.FieldLevel) bool {
|
||||
requestValue := fl.Field().Interface()
|
||||
params := strings.Fields(fl.Param())
|
||||
if len(params) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
tableName := params[0]
|
||||
fieldNames := params[1:]
|
||||
|
||||
query := r.DB.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), requestValue)
|
||||
for _, fieldName := range fieldNames[1:] {
|
||||
query = query.Or(fmt.Sprintf("%s = ?", fieldName), requestValue)
|
||||
}
|
||||
|
||||
var count int64
|
||||
err := query.Count(&count).Error
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return count != 0
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type NotExists struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func NewNotExists(db *gorm.DB) *NotExists {
|
||||
return &NotExists{DB: db}
|
||||
}
|
||||
|
||||
// NotExists 格式 `not_exists=categories id other_field`
|
||||
func (r *NotExists) NotExists(fl validator.FieldLevel) bool {
|
||||
requestValue := fl.Field().Interface()
|
||||
params := strings.Fields(fl.Param())
|
||||
if len(params) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
tableName := params[0]
|
||||
fieldNames := params[1:]
|
||||
|
||||
query := r.DB.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), requestValue)
|
||||
for _, fieldName := range fieldNames[1:] {
|
||||
query = query.Or(fmt.Sprintf("%s = ?", fieldName), requestValue)
|
||||
}
|
||||
|
||||
var count int64
|
||||
err := query.Count(&count).Error
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return count == 0
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type Password struct{}
|
||||
|
||||
func NewPassword() *Password {
|
||||
return &Password{}
|
||||
}
|
||||
|
||||
// Password 密码复杂度校验
|
||||
func (r *Password) Password(fl validator.FieldLevel) bool {
|
||||
password := fl.Field().String()
|
||||
// 不对空密码进行校验,有需要可以使用 required 标签
|
||||
if password == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
var hasUpper, hasLower, hasNumber, hasSpecial bool
|
||||
if len(password) < 8 || len(password) > 20 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, char := range password {
|
||||
switch {
|
||||
case unicode.IsUpper(char):
|
||||
hasUpper = true
|
||||
case unicode.IsLower(char):
|
||||
hasLower = true
|
||||
case unicode.IsNumber(char):
|
||||
hasNumber = true
|
||||
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
||||
hasSpecial = true
|
||||
}
|
||||
}
|
||||
|
||||
// 至少包含两类字符组合
|
||||
valid := (hasUpper && hasLower) ||
|
||||
(hasUpper && hasNumber) ||
|
||||
(hasUpper && hasSpecial) ||
|
||||
(hasLower && hasNumber) ||
|
||||
(hasLower && hasSpecial) ||
|
||||
(hasNumber && hasSpecial)
|
||||
|
||||
return valid
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type Regex struct{}
|
||||
|
||||
func NewRegex() *Regex {
|
||||
return &Regex{}
|
||||
}
|
||||
|
||||
func (r *Regex) Regex(fl validator.FieldLevel) bool {
|
||||
// 从标签中获取正则,格式类似于 `regex=^[a-zA-Z0-9_]+$`
|
||||
pattern := fl.Param()
|
||||
// 替换转义字符
|
||||
pattern = strings.ReplaceAll(pattern, ",", ",")
|
||||
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
value := fl.Field().String()
|
||||
return re.MatchString(value)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
)
|
||||
|
||||
func RegisterRules(v *validator.Validate) error {
|
||||
if err := v.RegisterValidation("exists", NewExists(app.Orm).Exists); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.RegisterValidation("not_exists", NewNotExists(app.Orm).NotExists); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.RegisterValidation("regex", NewRegex().Regex); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.RegisterValidation("password", NewPassword().Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.RegisterTranslation("exists", *app.Translator,
|
||||
func(ut ut.Translator) error {
|
||||
return ut.Add("exists", "{0} 不存在", true)
|
||||
},
|
||||
func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("exists", fe.Field())
|
||||
return t
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.RegisterTranslation("not_exists", *app.Translator,
|
||||
func(ut ut.Translator) error {
|
||||
return ut.Add("not_exists", "{0} 已存在", true)
|
||||
},
|
||||
func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("not_exists", fe.Field())
|
||||
return t
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.RegisterTranslation("regex", *app.Translator,
|
||||
func(ut ut.Translator) error {
|
||||
return ut.Add("regex", "{0} 格式不正确", true)
|
||||
},
|
||||
func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("regex", fe.Field())
|
||||
return t
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.RegisterTranslation("password", *app.Translator,
|
||||
func(ut ut.Translator) error {
|
||||
return ut.Add("password", "密码不满足要求(8-20位,至少包含字母、数字、特殊字符中的两种)", true)
|
||||
},
|
||||
func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("password")
|
||||
return t
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
pkgcert "github.com/TheTNB/panel/pkg/cert"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// CertRenew 证书续签
|
||||
@@ -23,8 +22,8 @@ func NewCertRenew() *CertRenew {
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver *CertRenew) Run() {
|
||||
if types.Status != types.StatusNormal {
|
||||
func (r *CertRenew) Run() {
|
||||
if app.Status != app.StatusNormal {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -50,7 +49,7 @@ func (receiver *CertRenew) Run() {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = receiver.certRepo.Renew(cert.ID)
|
||||
_, err = r.certRepo.Renew(cert.ID)
|
||||
if err != nil {
|
||||
app.Logger.Error("续签证书失败", zap.Error(err))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// Monitoring 系统监控
|
||||
@@ -24,8 +23,8 @@ func NewMonitoring() *Monitoring {
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver *Monitoring) Run() {
|
||||
if types.Status != types.StatusNormal {
|
||||
func (r *Monitoring) Run() {
|
||||
if app.Status != app.StatusNormal {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -33,18 +32,18 @@ func (receiver *Monitoring) Run() {
|
||||
//task := data.NewTaskRepo()
|
||||
//_ = task.DispatchWaiting()
|
||||
|
||||
monitor, err := receiver.settingRepo.Get(biz.SettingKeyMonitor)
|
||||
monitor, err := r.settingRepo.Get(biz.SettingKeyMonitor)
|
||||
if err != nil || !cast.ToBool(monitor) {
|
||||
return
|
||||
}
|
||||
|
||||
info := tools.GetMonitoringInfo()
|
||||
info := tools.CurrentInfo(nil, nil)
|
||||
|
||||
// 去除部分数据以减少数据库存储
|
||||
info.Disk = nil
|
||||
info.Cpus = nil
|
||||
|
||||
if types.Status != types.StatusNormal {
|
||||
if app.Status != app.StatusNormal {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,12 +53,12 @@ func (receiver *Monitoring) Run() {
|
||||
}
|
||||
|
||||
// 删除过期数据
|
||||
dayStr, err := receiver.settingRepo.Get(biz.SettingKeyMonitorDays)
|
||||
dayStr, err := r.settingRepo.Get(biz.SettingKeyMonitorDays)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
day := cast.ToInt(dayStr)
|
||||
if day <= 0 || types.Status != types.StatusNormal {
|
||||
if day <= 0 || app.Status != app.StatusNormal {
|
||||
return
|
||||
}
|
||||
if err = app.Orm.Where("created_at < ?", time.Now().AddDate(0, 0, -day).Format("2006-01-02 15:04:05")).Delete(&biz.Monitor{}).Error; err != nil {
|
||||
|
||||
@@ -9,56 +9,59 @@ import (
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// PanelTask 面板每日任务
|
||||
type PanelTask struct {
|
||||
appRepo biz.AppRepo
|
||||
backupRepo biz.BackupRepo
|
||||
appRepo biz.AppRepo
|
||||
backupRepo biz.BackupRepo
|
||||
settingRepo biz.SettingRepo
|
||||
}
|
||||
|
||||
func NewPanelTask() *PanelTask {
|
||||
return &PanelTask{
|
||||
appRepo: data.NewAppRepo(),
|
||||
backupRepo: data.NewBackupRepo(),
|
||||
appRepo: data.NewAppRepo(),
|
||||
backupRepo: data.NewBackupRepo(),
|
||||
settingRepo: data.NewSettingRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver *PanelTask) Run() {
|
||||
types.Status = types.StatusMaintain
|
||||
func (r *PanelTask) Run() {
|
||||
app.Status = app.StatusMaintain
|
||||
|
||||
// 优化数据库
|
||||
if err := app.Orm.Exec("VACUUM").Error; err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
app.Status = app.StatusFailed
|
||||
app.Logger.Error("优化面板数据库失败", zap.Error(err))
|
||||
}
|
||||
if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
app.Status = app.StatusFailed
|
||||
app.Logger.Error("优化面板数据库失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 备份面板
|
||||
if err := receiver.backupRepo.Create(biz.BackupTypePanel, ""); err != nil {
|
||||
if err := r.backupRepo.Create(biz.BackupTypePanel, ""); err != nil {
|
||||
app.Logger.Error("备份面板失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理备份
|
||||
path, err := receiver.backupRepo.GetPath("panel")
|
||||
path, err := r.backupRepo.GetPath("panel")
|
||||
if err == nil {
|
||||
if err = receiver.backupRepo.ClearExpired(path, "panel_", 10); err != nil {
|
||||
if err = r.backupRepo.ClearExpired(path, "panel_", 10); err != nil {
|
||||
app.Logger.Error("清理面板备份失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 更新商店缓存
|
||||
if err = receiver.appRepo.UpdateCache(); err != nil {
|
||||
app.Logger.Error("更新商店缓存失败", zap.Error(err))
|
||||
if offline, err := r.settingRepo.GetBool(biz.SettingKeyOfflineMode); err == nil && !offline {
|
||||
if err = r.appRepo.UpdateCache(); err != nil {
|
||||
app.Logger.Error("更新商店缓存失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 回收内存
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
|
||||
types.Status = types.StatusNormal
|
||||
app.Status = app.StatusNormal
|
||||
}
|
||||
|
||||
@@ -20,19 +20,19 @@ func NewProcessTask(taskRepo biz.TaskRepo) *ProcessTask {
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver *ProcessTask) Handle(args ...any) error {
|
||||
func (r *ProcessTask) Handle(args ...any) error {
|
||||
taskID, ok := args[0].(uint)
|
||||
if !ok {
|
||||
return errors.New("参数错误")
|
||||
}
|
||||
receiver.taskID = taskID
|
||||
r.taskID = taskID
|
||||
|
||||
task, err := receiver.taskRepo.Get(taskID)
|
||||
task, err := r.taskRepo.Get(taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = receiver.taskRepo.UpdateStatus(taskID, biz.TaskStatusRunning); err != nil {
|
||||
if err = r.taskRepo.UpdateStatus(taskID, biz.TaskStatusRunning); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -40,13 +40,13 @@ func (receiver *ProcessTask) Handle(args ...any) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = receiver.taskRepo.UpdateStatus(taskID, biz.TaskStatusSuccess); err != nil {
|
||||
if err = r.taskRepo.UpdateStatus(taskID, biz.TaskStatusSuccess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (receiver *ProcessTask) ErrHandle(err error) {
|
||||
_ = receiver.taskRepo.UpdateStatus(receiver.taskID, biz.TaskStatusFailed)
|
||||
func (r *ProcessTask) ErrHandle(err error) {
|
||||
_ = r.taskRepo.UpdateStatus(r.taskID, biz.TaskStatusFailed)
|
||||
}
|
||||
|
||||
@@ -26,9 +26,14 @@ func Cli() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Usage: "升级面板",
|
||||
Usage: "更新面板",
|
||||
Action: cliService.Update,
|
||||
},
|
||||
{
|
||||
Name: "fix",
|
||||
Usage: "修复面板",
|
||||
Action: cliService.Fix,
|
||||
},
|
||||
{
|
||||
Name: "info",
|
||||
Usage: "输出面板基本信息",
|
||||
@@ -322,6 +327,11 @@ func Cli() []*cli.Command {
|
||||
Usage: "卸载应用",
|
||||
Action: cliService.AppUnInstall,
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Usage: "更新应用",
|
||||
Action: cliService.AppUpdate,
|
||||
},
|
||||
{
|
||||
Name: "write",
|
||||
Usage: "添加面板应用标记(仅限指导下使用)",
|
||||
@@ -362,7 +372,12 @@ func Cli() []*cli.Command {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "clearTask",
|
||||
Name: "sync-time",
|
||||
Usage: "同步系统时间",
|
||||
Action: cliService.SyncTime,
|
||||
},
|
||||
{
|
||||
Name: "clear-task",
|
||||
Usage: "清理面板任务队列(仅限指导下使用)",
|
||||
Hidden: true,
|
||||
Action: cliService.ClearTask,
|
||||
|
||||
@@ -23,18 +23,18 @@ func Http(r chi.Router) {
|
||||
r.With(middleware.MustLogin).Get("/info", user.Info)
|
||||
})
|
||||
|
||||
r.Route("/info", func(r chi.Router) {
|
||||
info := service.NewInfoService()
|
||||
r.Get("/panel", info.Panel)
|
||||
r.With(middleware.MustLogin).Get("/homeApps", info.HomeApps)
|
||||
r.With(middleware.MustLogin).Get("/realtime", info.Realtime)
|
||||
r.With(middleware.MustLogin).Get("/systemInfo", info.SystemInfo)
|
||||
r.With(middleware.MustLogin).Get("/countInfo", info.CountInfo)
|
||||
r.With(middleware.MustLogin).Get("/installedDbAndPhp", info.InstalledDbAndPhp)
|
||||
r.With(middleware.MustLogin).Get("/checkUpdate", info.CheckUpdate)
|
||||
r.With(middleware.MustLogin).Get("/updateInfo", info.UpdateInfo)
|
||||
r.With(middleware.MustLogin).Post("/update", info.Update)
|
||||
r.With(middleware.MustLogin).Post("/restart", info.Restart)
|
||||
r.Route("/dashboard", func(r chi.Router) {
|
||||
dashboard := service.NewDashboardService()
|
||||
r.Get("/panel", dashboard.Panel)
|
||||
r.With(middleware.MustLogin).Get("/homeApps", dashboard.HomeApps)
|
||||
r.With(middleware.MustLogin).Post("/current", dashboard.Current)
|
||||
r.With(middleware.MustLogin).Get("/systemInfo", dashboard.SystemInfo)
|
||||
r.With(middleware.MustLogin).Get("/countInfo", dashboard.CountInfo)
|
||||
r.With(middleware.MustLogin).Get("/installedDbAndPhp", dashboard.InstalledDbAndPhp)
|
||||
r.With(middleware.MustLogin).Get("/checkUpdate", dashboard.CheckUpdate)
|
||||
r.With(middleware.MustLogin).Get("/updateInfo", dashboard.UpdateInfo)
|
||||
r.With(middleware.MustLogin).Post("/update", dashboard.Update)
|
||||
r.With(middleware.MustLogin).Post("/restart", dashboard.Restart)
|
||||
})
|
||||
|
||||
r.Route("/task", func(r chi.Router) {
|
||||
@@ -148,6 +148,12 @@ func Http(r chi.Router) {
|
||||
r.Get("/rule", firewall.GetRules)
|
||||
r.Post("/rule", firewall.CreateRule)
|
||||
r.Delete("/rule", firewall.DeleteRule)
|
||||
r.Get("/ipRule", firewall.GetIPRules)
|
||||
r.Post("/ipRule", firewall.CreateIPRule)
|
||||
r.Delete("/ipRule", firewall.DeleteIPRule)
|
||||
r.Get("/forward", firewall.GetForwards)
|
||||
r.Post("/forward", firewall.CreateForward)
|
||||
r.Delete("/forward", firewall.DeleteForward)
|
||||
})
|
||||
|
||||
r.Route("/ssh", func(r chi.Router) {
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
)
|
||||
|
||||
type AppService struct {
|
||||
appRepo biz.AppRepo
|
||||
appRepo biz.AppRepo
|
||||
settingRepo biz.SettingRepo
|
||||
}
|
||||
|
||||
func NewAppService() *AppService {
|
||||
return &AppService{
|
||||
appRepo: data.NewAppRepo(),
|
||||
appRepo: data.NewAppRepo(),
|
||||
settingRepo: data.NewSettingRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +47,7 @@ func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
|
||||
show = installedAppMap[item.Slug].Show
|
||||
}
|
||||
apps = append(apps, types.AppCenter{
|
||||
Icon: item.Icon,
|
||||
Name: item.Name,
|
||||
Description: item.Description,
|
||||
Slug: item.Slug,
|
||||
@@ -162,6 +165,11 @@ func (s *AppService) IsInstalled(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *AppService) UpdateCache(w http.ResponseWriter, r *http.Request) {
|
||||
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
|
||||
Error(w, http.StatusForbidden, "离线模式下无法更新应用列表缓存")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.appRepo.UpdateCache(); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
|
||||
@@ -68,9 +68,11 @@ func Bind[T any](r *http.Request) (*T, error) {
|
||||
if err := binder.Query(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if slices.Contains([]string{"POST", "PUT", "PATCH"}, strings.ToUpper(r.Method)) {
|
||||
if err := binder.Body(req); err != nil {
|
||||
return nil, err
|
||||
if slices.Contains([]string{"POST", "PUT", "PATCH", "DELETE"}, strings.ToUpper(r.Method)) {
|
||||
if r.ContentLength > 0 {
|
||||
if err := binder.Body(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,16 +55,16 @@ func (s *CertService) CAProviders(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *CertService) DNSProviders(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, []types.LV{
|
||||
{
|
||||
Label: "DNSPod",
|
||||
Value: string(acme.DnsPod),
|
||||
Label: "阿里云",
|
||||
Value: string(acme.AliYun),
|
||||
},
|
||||
{
|
||||
Label: "腾讯云",
|
||||
Value: string(acme.Tencent),
|
||||
},
|
||||
{
|
||||
Label: "阿里云",
|
||||
Value: string(acme.AliYun),
|
||||
Label: "华为云",
|
||||
Value: string(acme.Huawei),
|
||||
},
|
||||
{
|
||||
Label: "CloudFlare",
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/ntp"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
@@ -92,6 +93,10 @@ func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error {
|
||||
return s.settingRepo.UpdatePanel(ver, url, checksum)
|
||||
}
|
||||
|
||||
func (s *CliService) Fix(ctx context.Context, cmd *cli.Command) error {
|
||||
return s.settingRepo.FixPanel()
|
||||
}
|
||||
|
||||
func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error {
|
||||
user := new(biz.User)
|
||||
if err := app.Orm.Where("id", 1).First(user).Error; err != nil {
|
||||
@@ -526,8 +531,8 @@ func (s *CliService) CutoffClear(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
func (s *CliService) AppInstall(ctx context.Context, cmd *cli.Command) error {
|
||||
channel := cmd.Args().Get(0)
|
||||
slug := cmd.Args().Get(1)
|
||||
slug := cmd.Args().First()
|
||||
channel := cmd.Args().Get(1)
|
||||
if channel == "" || slug == "" {
|
||||
return fmt.Errorf("参数不能为空")
|
||||
}
|
||||
@@ -536,7 +541,7 @@ func (s *CliService) AppInstall(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("应用安装失败:%v", err)
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("已创建应用 %s 安装任务", slug))
|
||||
color.Greenln(fmt.Sprintf("应用 %s 安装完成", slug))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -551,7 +556,22 @@ func (s *CliService) AppUnInstall(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("应用卸载失败:%v", err)
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("已创建应用 %s 卸载任务", slug))
|
||||
color.Greenln(fmt.Sprintf("应用 %s 卸载完成", slug))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) AppUpdate(ctx context.Context, cmd *cli.Command) error {
|
||||
slug := cmd.Args().First()
|
||||
if slug == "" {
|
||||
return fmt.Errorf("参数不能为空")
|
||||
}
|
||||
|
||||
if err := s.appRepo.Update(slug); err != nil {
|
||||
return fmt.Errorf("应用更新失败:%v", err)
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("应用 %s 更新完成", slug))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -593,6 +613,20 @@ func (s *CliService) AppRemove(ctx context.Context, cmd *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) SyncTime(ctx context.Context, cmd *cli.Command) error {
|
||||
now, err := ntp.Now()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ntp.UpdateSystemTime(now); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("时间同步成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) ClearTask(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := app.Orm.Model(&biz.Task{}).
|
||||
Where("status", biz.TaskStatusRunning).Or("status", biz.TaskStatusWaiting).
|
||||
@@ -601,6 +635,7 @@ func (s *CliService) ClearTask(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("任务清理失败:%v", err)
|
||||
}
|
||||
|
||||
color.Greenln("任务清理成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,21 @@ package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
@@ -21,7 +25,7 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type InfoService struct {
|
||||
type DashboardService struct {
|
||||
api *api.API
|
||||
taskRepo biz.TaskRepo
|
||||
websiteRepo biz.WebsiteRepo
|
||||
@@ -30,8 +34,8 @@ type InfoService struct {
|
||||
cronRepo biz.CronRepo
|
||||
}
|
||||
|
||||
func NewInfoService() *InfoService {
|
||||
return &InfoService{
|
||||
func NewDashboardService() *DashboardService {
|
||||
return &DashboardService{
|
||||
api: api.NewAPI(app.Version),
|
||||
taskRepo: data.NewTaskRepo(),
|
||||
websiteRepo: data.NewWebsiteRepo(),
|
||||
@@ -41,7 +45,7 @@ func NewInfoService() *InfoService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InfoService) Panel(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *DashboardService) Panel(w http.ResponseWriter, r *http.Request) {
|
||||
name, _ := s.settingRepo.Get(biz.SettingKeyName)
|
||||
if name == "" {
|
||||
name = "耗子面板"
|
||||
@@ -53,31 +57,67 @@ func (s *InfoService) Panel(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *InfoService) HomeApps(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *DashboardService) HomeApps(w http.ResponseWriter, r *http.Request) {
|
||||
apps, err := s.appRepo.GetHomeShow()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "获取首页应用失败")
|
||||
Error(w, http.StatusInternalServerError, "获取首页应用失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, apps)
|
||||
}
|
||||
|
||||
func (s *InfoService) Realtime(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, tools.GetMonitoringInfo())
|
||||
func (s *DashboardService) Current(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.DashboardCurrent](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, tools.CurrentInfo(req.Nets, req.Disks))
|
||||
}
|
||||
|
||||
func (s *InfoService) SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
monitorInfo := tools.GetMonitoringInfo()
|
||||
func (s *DashboardService) SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
hostInfo, err := host.Info()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "获取系统信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 所有网卡名称
|
||||
var nets []types.LV
|
||||
netInterfaces, _ := net.Interfaces()
|
||||
for _, v := range netInterfaces {
|
||||
nets = append(nets, types.LV{
|
||||
Value: v.Name,
|
||||
Label: v.Name,
|
||||
})
|
||||
}
|
||||
// 所有硬盘名称
|
||||
var disks []types.LV
|
||||
partitions, _ := disk.Partitions(false)
|
||||
for _, v := range partitions {
|
||||
disks = append(disks, types.LV{
|
||||
Value: v.Device,
|
||||
Label: fmt.Sprintf("%s (%s)", v.Device, v.Mountpoint),
|
||||
})
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion,
|
||||
"uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400),
|
||||
"panel_version": app.Version,
|
||||
"procs": hostInfo.Procs,
|
||||
"hostname": hostInfo.Hostname,
|
||||
"panel_version": app.Version,
|
||||
"kernel_arch": hostInfo.KernelArch,
|
||||
"kernel_version": hostInfo.KernelVersion,
|
||||
"os_name": hostInfo.Platform + " " + hostInfo.PlatformVersion,
|
||||
"boot_time": hostInfo.BootTime,
|
||||
"uptime": hostInfo.Uptime,
|
||||
"nets": nets,
|
||||
"disks": disks,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *InfoService) CountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *DashboardService) CountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
websiteCount, err := s.websiteRepo.Count()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "获取网站数量失败")
|
||||
@@ -173,7 +213,7 @@ func (s *InfoService) CountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *InfoService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *DashboardService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request) {
|
||||
mysqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "mysql%")
|
||||
postgresqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "postgresql%")
|
||||
php, _ := s.appRepo.GetInstalledAll("slug like ?", "php%")
|
||||
@@ -206,7 +246,12 @@ func (s *InfoService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *InfoService) CheckUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *DashboardService) CheckUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
|
||||
Error(w, http.StatusForbidden, "离线模式下无法检查更新")
|
||||
return
|
||||
}
|
||||
|
||||
current := app.Version
|
||||
latest, err := s.api.LatestVersion()
|
||||
if err != nil {
|
||||
@@ -236,7 +281,12 @@ func (s *InfoService) CheckUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *DashboardService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
|
||||
Error(w, http.StatusForbidden, "离线模式下无法检查更新")
|
||||
return
|
||||
}
|
||||
|
||||
current := app.Version
|
||||
latest, err := s.api.LatestVersion()
|
||||
if err != nil {
|
||||
@@ -268,14 +318,14 @@ func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, versions)
|
||||
}
|
||||
|
||||
func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
if s.taskRepo.HasRunningTask() {
|
||||
Error(w, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
|
||||
func (s *DashboardService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
|
||||
Error(w, http.StatusForbidden, "离线模式下无法更新")
|
||||
return
|
||||
}
|
||||
if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE)").Error; err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
Error(w, http.StatusInternalServerError, "面板数据库异常,已终止操作:%v", err)
|
||||
|
||||
if s.taskRepo.HasRunningTask() {
|
||||
Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止更新,请稍后再试")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -292,21 +342,21 @@ func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
ver, url, checksum := panel.Version, download.URL, download.Checksum
|
||||
|
||||
types.Status = types.StatusUpgrade
|
||||
app.Status = app.StatusUpgrade
|
||||
if err = s.settingRepo.UpdatePanel(ver, url, checksum); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
app.Status = app.StatusFailed
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
types.Status = types.StatusNormal
|
||||
app.Status = app.StatusNormal
|
||||
Success(w, nil)
|
||||
tools.RestartPanel()
|
||||
}
|
||||
|
||||
func (s *InfoService) Restart(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *DashboardService) Restart(w http.ResponseWriter, r *http.Request) {
|
||||
if s.taskRepo.HasRunningTask() {
|
||||
Error(w, http.StatusInternalServerError, "当前有任务正在执行,禁止重启")
|
||||
Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止重启,请稍后再试")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/firewall"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
@@ -15,7 +17,6 @@ type FirewallService struct {
|
||||
}
|
||||
|
||||
func NewFirewallService() *FirewallService {
|
||||
|
||||
return &FirewallService{
|
||||
firewall: firewall.NewFirewall(),
|
||||
}
|
||||
@@ -65,7 +66,38 @@ func (s *FirewallService) GetRules(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, rules)
|
||||
var filledRules []map[string]any
|
||||
for rule := range slices.Values(rules) {
|
||||
// 去除IP规则
|
||||
if rule.PortStart == 1 && rule.PortEnd == 65535 {
|
||||
continue
|
||||
}
|
||||
isUse := false
|
||||
for port := rule.PortStart; port <= rule.PortEnd; port++ {
|
||||
if rule.Protocol == firewall.ProtocolTCP {
|
||||
isUse = os.TCPPortInUse(port)
|
||||
} else if rule.Protocol == firewall.ProtocolUDP {
|
||||
isUse = os.UDPPortInUse(port)
|
||||
} else {
|
||||
isUse = os.TCPPortInUse(port) || os.UDPPortInUse(port)
|
||||
}
|
||||
if isUse {
|
||||
break
|
||||
}
|
||||
}
|
||||
filledRules = append(filledRules, map[string]any{
|
||||
"family": rule.Family,
|
||||
"port_start": rule.PortStart,
|
||||
"port_end": rule.PortEnd,
|
||||
"protocol": rule.Protocol,
|
||||
"address": rule.Address,
|
||||
"strategy": rule.Strategy,
|
||||
"direction": rule.Direction,
|
||||
"in_use": isUse,
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, filledRules)
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
@@ -74,13 +106,15 @@ func (s *FirewallService) GetRules(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *FirewallService) CreateRule(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallCreateRule](r)
|
||||
req, err := Bind[request.FirewallRule](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.Port(firewall.FireInfo{Port: req.Port, Protocol: req.Protocol}, "add"); err != nil {
|
||||
if err = s.firewall.Port(firewall.FireInfo{
|
||||
Family: req.Family, PortStart: req.PortStart, PortEnd: req.PortEnd, Protocol: firewall.Protocol(req.Protocol), Address: req.Address, Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
|
||||
}, firewall.OperationAdd); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -89,13 +123,128 @@ func (s *FirewallService) CreateRule(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *FirewallService) DeleteRule(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallCreateRule](r)
|
||||
req, err := Bind[request.FirewallRule](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.Port(firewall.FireInfo{Port: req.Port, Protocol: req.Protocol}, "remove"); err != nil {
|
||||
if err = s.firewall.Port(firewall.FireInfo{
|
||||
Family: req.Family, PortStart: req.PortStart, PortEnd: req.PortEnd, Protocol: firewall.Protocol(req.Protocol), Address: req.Address, Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
|
||||
}, firewall.OperationRemove); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FirewallService) GetIPRules(w http.ResponseWriter, r *http.Request) {
|
||||
rules, err := s.firewall.ListRule()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var filledRules []map[string]any
|
||||
for rule := range slices.Values(rules) {
|
||||
// 保留IP规则
|
||||
if rule.PortStart != 1 || rule.PortEnd != 65535 || rule.Address == "" {
|
||||
continue
|
||||
}
|
||||
filledRules = append(filledRules, map[string]any{
|
||||
"family": rule.Family,
|
||||
"protocol": rule.Protocol,
|
||||
"address": rule.Address,
|
||||
"strategy": rule.Strategy,
|
||||
"direction": rule.Direction,
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, filledRules)
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FirewallService) CreateIPRule(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallIPRule](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.RichRules(firewall.FireInfo{
|
||||
Family: req.Family, Address: req.Address, Protocol: firewall.Protocol(req.Protocol), Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
|
||||
}, firewall.OperationAdd); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FirewallService) DeleteIPRule(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallIPRule](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.RichRules(firewall.FireInfo{
|
||||
Family: req.Family, Address: req.Address, Protocol: firewall.Protocol(req.Protocol), Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
|
||||
}, firewall.OperationRemove); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FirewallService) GetForwards(w http.ResponseWriter, r *http.Request) {
|
||||
forwards, err := s.firewall.ListForward()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, forwards)
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FirewallService) CreateForward(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallForward](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.Forward(firewall.Forward{
|
||||
Protocol: firewall.Protocol(req.Protocol), Port: req.Port, TargetIP: req.TargetIP, TargetPort: req.TargetPort,
|
||||
}, firewall.OperationAdd); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FirewallService) DeleteForward(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallForward](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.Forward(firewall.Forward{
|
||||
Protocol: firewall.Protocol(req.Protocol), Port: req.Port, TargetIP: req.TargetIP, TargetPort: req.TargetPort,
|
||||
}, firewall.OperationRemove); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) {
|
||||
list.Load.Load1 = append(list.Load.Load1, monitor.Info.Load.Load1)
|
||||
list.Load.Load5 = append(list.Load.Load5, monitor.Info.Load.Load5)
|
||||
list.Load.Load15 = append(list.Load.Load15, monitor.Info.Load.Load15)
|
||||
list.CPU.Percent = append(list.CPU.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent[0]))
|
||||
list.CPU.Percent = append(list.CPU.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent))
|
||||
list.Mem.Available = append(list.Mem.Available, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Available)/1024/1024))
|
||||
list.Mem.Used = append(list.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024))
|
||||
list.SWAP.Used = append(list.SWAP.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
|
||||
|
||||
@@ -51,17 +51,16 @@ func (c *Client) UseManualDns(total int, check ...bool) {
|
||||
// UseHTTP 使用 HTTP 验证
|
||||
// conf nginx 配置文件路径
|
||||
// path 验证文件存放路径
|
||||
func (c *Client) UseHTTP(conf, path string) {
|
||||
func (c *Client) UseHTTP(conf string) {
|
||||
c.zClient.ChallengeSolvers = map[string]acmez.Solver{
|
||||
acme.ChallengeTypeHTTP01: httpSolver{
|
||||
conf: conf,
|
||||
path: path,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ObtainSSL 签发 SSL 证书
|
||||
func (c *Client) ObtainSSL(ctx context.Context, domains []string, keyType KeyType) (Certificate, error) {
|
||||
// ObtainCertificate 签发 SSL 证书
|
||||
func (c *Client) ObtainCertificate(ctx context.Context, domains []string, keyType KeyType) (Certificate, error) {
|
||||
certPrivateKey, err := generatePrivateKey(keyType)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
@@ -76,12 +75,12 @@ func (c *Client) ObtainSSL(ctx context.Context, domains []string, keyType KeyTyp
|
||||
return Certificate{}, err
|
||||
}
|
||||
|
||||
cert := c.selectPreferredChain(certs)
|
||||
return Certificate{PrivateKey: pemPrivateKey, Certificate: cert}, nil
|
||||
crt := c.selectPreferredChain(certs)
|
||||
return Certificate{PrivateKey: pemPrivateKey, Certificate: crt}, nil
|
||||
}
|
||||
|
||||
// ObtainSSLManual 手动验证 SSL 证书
|
||||
func (c *Client) ObtainSSLManual() (Certificate, error) {
|
||||
// ObtainCertificateManual 手动验证 SSL 证书
|
||||
func (c *Client) ObtainCertificateManual() (Certificate, error) {
|
||||
// 发送信号,开始验证
|
||||
c.controlChan <- struct{}{}
|
||||
// 等待验证完成
|
||||
@@ -94,20 +93,20 @@ func (c *Client) ObtainSSLManual() (Certificate, error) {
|
||||
return data.(Certificate), nil
|
||||
}
|
||||
|
||||
// RenewSSL 续签 SSL 证书
|
||||
func (c *Client) RenewSSL(ctx context.Context, certUrl string, domains []string, keyType KeyType) (Certificate, error) {
|
||||
// RenewCertificate 续签 SSL 证书
|
||||
func (c *Client) RenewCertificate(ctx context.Context, certUrl string, domains []string, keyType KeyType) (Certificate, error) {
|
||||
_, err := c.zClient.GetCertificateChain(ctx, c.Account, certUrl)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
|
||||
return c.ObtainSSL(ctx, domains, keyType)
|
||||
return c.ObtainCertificate(ctx, domains, keyType)
|
||||
}
|
||||
|
||||
// GetDNSRecords 获取 DNS 解析(手动设置)
|
||||
func (c *Client) GetDNSRecords(ctx context.Context, domains []string, keyType KeyType) ([]DNSRecord, error) {
|
||||
go func(ctx context.Context, domains []string, keyType KeyType) {
|
||||
certs, err := c.ObtainSSL(ctx, domains, keyType)
|
||||
certs, err := c.ObtainCertificate(ctx, domains, keyType)
|
||||
// 将证书和错误信息发送到 dataChan
|
||||
if err != nil {
|
||||
c.dataChan <- err
|
||||
|
||||
@@ -20,9 +20,9 @@ func (s *ClientTestSuite) TestObtainSSL() {
|
||||
client, err := NewRegisterAccount(ctx, "ci@haozi.net", CALetsEncryptStaging, nil, KeyEC256)
|
||||
s.Nil(err)
|
||||
|
||||
client.UseDns(DnsPod, DNSParam{
|
||||
ID: "123456",
|
||||
Token: "654321",
|
||||
client.UseDns(AliYun, DNSParam{
|
||||
AK: "123456",
|
||||
SK: "654321",
|
||||
})
|
||||
|
||||
/*client.UseManualDns(2)
|
||||
@@ -34,8 +34,8 @@ func (s *ClientTestSuite) TestObtainSSL() {
|
||||
|
||||
time.Sleep(2 * time.Minute)
|
||||
|
||||
ssl, err := client.ObtainSSLManual()*/
|
||||
ssl, err := client.ObtainSSL(ctx, []string{"*.haozi.net", "haozi.net"}, KeyEC256)
|
||||
ssl, err := client.ObtainCertificateManual()*/
|
||||
ssl, err := client.ObtainCertificate(ctx, []string{"*.haozi.net", "haozi.net"}, KeyEC256)
|
||||
s.Error(err)
|
||||
s.NotNil(ssl)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/libdns/alidns"
|
||||
"github.com/libdns/cloudflare"
|
||||
"github.com/libdns/dnspod"
|
||||
"github.com/libdns/huaweicloud"
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/libdns/tencentcloud"
|
||||
"github.com/mholt/acmez/v2/acme"
|
||||
@@ -21,35 +21,20 @@ import (
|
||||
|
||||
type httpSolver struct {
|
||||
conf string
|
||||
path string
|
||||
}
|
||||
|
||||
func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error {
|
||||
var err error
|
||||
if s.path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
challengeFilePath := filepath.Join(s.path, challenge.HTTP01ResourcePath())
|
||||
if err = os.MkdirAll(filepath.Dir(challengeFilePath), 0755); err != nil {
|
||||
return fmt.Errorf("无法在网站目录创建HTTP挑战所需的目录: %w", err)
|
||||
}
|
||||
|
||||
if err = os.WriteFile(challengeFilePath, []byte(challenge.KeyAuthorization), 0644); err != nil {
|
||||
return fmt.Errorf("无法在网站目录创建HTTP挑战所需的文件: %w", err)
|
||||
}
|
||||
|
||||
conf := fmt.Sprintf(`location = /.well-known/acme-challenge/%s {
|
||||
conf := fmt.Sprintf(`location = %s {
|
||||
default_type text/plain;
|
||||
return 200 %q;
|
||||
}
|
||||
`, challenge.Token, challenge.KeyAuthorization)
|
||||
if err = os.WriteFile(s.conf, []byte(conf), 0644); err != nil {
|
||||
return fmt.Errorf("无法写入Nginx配置文件: %w", err)
|
||||
`, challenge.HTTP01ResourcePath(), challenge.KeyAuthorization)
|
||||
if err := os.WriteFile(s.conf, []byte(conf), 0644); err != nil {
|
||||
return fmt.Errorf("无法写入 Nginx 配置文件: %w", err)
|
||||
}
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
if err := systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return fmt.Errorf("无法重载Nginx: %w", err)
|
||||
return fmt.Errorf("无法重载 Nginx: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -57,11 +42,6 @@ func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error {
|
||||
|
||||
// CleanUp cleans up the HTTP server if it is the last one to finish.
|
||||
func (s httpSolver) CleanUp(_ context.Context, challenge acme.Challenge) error {
|
||||
if s.path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = os.Remove(filepath.Join(s.path, challenge.HTTP01ResourcePath()))
|
||||
_ = os.WriteFile(s.conf, []byte{}, 0644)
|
||||
_ = systemctl.Reload("nginx")
|
||||
return nil
|
||||
@@ -78,11 +58,11 @@ func (s dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error
|
||||
keyAuth := challenge.DNS01KeyAuthorization()
|
||||
provider, err := s.getDNSProvider()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取DNS提供商失败: %w", err)
|
||||
return fmt.Errorf("获取 DNS 提供商失败: %w", err)
|
||||
}
|
||||
zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取域名%q的顶级域失败: %w", dnsName, err)
|
||||
return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err)
|
||||
}
|
||||
|
||||
rec := libdns.Record{
|
||||
@@ -91,12 +71,12 @@ func (s dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error
|
||||
Value: keyAuth,
|
||||
}
|
||||
|
||||
results, err := provider.AppendRecords(ctx, zone+".", []libdns.Record{rec})
|
||||
results, err := provider.SetRecords(ctx, zone+".", []libdns.Record{rec})
|
||||
if err != nil {
|
||||
return fmt.Errorf("域名%q添加临时记录%q失败: %w", zone, dnsName, err)
|
||||
return fmt.Errorf("域名 %q 添加临时记录 %q 失败: %w", zone, dnsName, err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
return fmt.Errorf("预期添加1条记录,但实际添加了%d条记录", len(results))
|
||||
return fmt.Errorf("预期添加 1 条记录,但实际添加了 %d 条记录", len(results))
|
||||
}
|
||||
|
||||
s.records = &results
|
||||
@@ -107,13 +87,16 @@ func (s dnsSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error
|
||||
dnsName := challenge.DNS01TXTRecordName()
|
||||
provider, err := s.getDNSProvider()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取DNS提供商失败: %w", err)
|
||||
return fmt.Errorf("获取 DNS 提供商失败: %w", err)
|
||||
}
|
||||
zone, _ := publicsuffix.EffectiveTLDPlusOne(dnsName)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err)
|
||||
}
|
||||
_, _ = provider.DeleteRecords(ctx, zone+".", *s.records)
|
||||
return nil
|
||||
}
|
||||
@@ -122,23 +105,24 @@ func (s dnsSolver) getDNSProvider() (DNSProvider, error) {
|
||||
var dns DNSProvider
|
||||
|
||||
switch s.dns {
|
||||
case DnsPod:
|
||||
dns = &dnspod.Provider{
|
||||
APIToken: s.param.ID + "," + s.param.Token,
|
||||
case AliYun:
|
||||
dns = &alidns.Provider{
|
||||
AccKeyID: s.param.AK,
|
||||
AccKeySecret: s.param.SK,
|
||||
}
|
||||
case Tencent:
|
||||
dns = &tencentcloud.Provider{
|
||||
SecretId: s.param.AccessKey,
|
||||
SecretKey: s.param.SecretKey,
|
||||
SecretId: s.param.AK,
|
||||
SecretKey: s.param.SK,
|
||||
}
|
||||
case AliYun:
|
||||
dns = &alidns.Provider{
|
||||
AccKeyID: s.param.AccessKey,
|
||||
AccKeySecret: s.param.SecretKey,
|
||||
case Huawei:
|
||||
dns = &huaweicloud.Provider{
|
||||
AccessKeyId: s.param.AK,
|
||||
SecretAccessKey: s.param.SK,
|
||||
}
|
||||
case CloudFlare:
|
||||
dns = &cloudflare.Provider{
|
||||
APIToken: s.param.APIkey,
|
||||
APIToken: s.param.AK,
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("未知的DNS提供商 %q", s.dns)
|
||||
@@ -150,22 +134,19 @@ func (s dnsSolver) getDNSProvider() (DNSProvider, error) {
|
||||
type DnsType string
|
||||
|
||||
const (
|
||||
DnsPod DnsType = "dnspod"
|
||||
Tencent DnsType = "tencent"
|
||||
AliYun DnsType = "aliyun"
|
||||
Huawei DnsType = "huawei"
|
||||
CloudFlare DnsType = "cloudflare"
|
||||
)
|
||||
|
||||
type DNSParam struct {
|
||||
ID string `form:"id" json:"id"`
|
||||
Token string `form:"token" json:"token"`
|
||||
AccessKey string `form:"access_key" json:"access_key"`
|
||||
SecretKey string `form:"secret_key" json:"secret_key"`
|
||||
APIkey string `form:"api_key" json:"api_key"`
|
||||
AK string `form:"ak" json:"ak"`
|
||||
SK string `form:"sk" json:"sk"`
|
||||
}
|
||||
|
||||
type DNSProvider interface {
|
||||
libdns.RecordAppender
|
||||
libdns.RecordSetter
|
||||
libdns.RecordDeleter
|
||||
}
|
||||
|
||||
@@ -177,18 +158,20 @@ type manualDNSSolver struct {
|
||||
}
|
||||
|
||||
func (s manualDNSSolver) Present(ctx context.Context, challenge acme.Challenge) error {
|
||||
dnsName := challenge.DNS01TXTRecordName()
|
||||
full := challenge.DNS01TXTRecordName()
|
||||
keyAuth := challenge.DNS01KeyAuthorization()
|
||||
domain, err := publicsuffix.EffectiveTLDPlusOne(full)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取 %q 的顶级域失败: %w", full, err)
|
||||
}
|
||||
|
||||
*s.records = append(*s.records, DNSRecord{
|
||||
Key: dnsName,
|
||||
Value: keyAuth,
|
||||
Name: strings.TrimSuffix(full, "."+domain),
|
||||
Domain: domain,
|
||||
Value: keyAuth,
|
||||
})
|
||||
s.dataChan <- *s.records
|
||||
|
||||
_, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
<-s.controlChan
|
||||
return nil
|
||||
}
|
||||
@@ -198,6 +181,7 @@ func (s manualDNSSolver) CleanUp(_ context.Context, _ acme.Challenge) error {
|
||||
}
|
||||
|
||||
type DNSRecord struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Name string `json:"name"`
|
||||
Domain string `json:"domain"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
@@ -1,24 +1,55 @@
|
||||
package firewall
|
||||
|
||||
type Operation string
|
||||
|
||||
var (
|
||||
OperationAdd Operation = "add" // 添加
|
||||
OperationRemove Operation = "remove" // 移除
|
||||
)
|
||||
|
||||
type Protocol string
|
||||
|
||||
var (
|
||||
ProtocolTCP Protocol = "tcp" // tcp
|
||||
ProtocolUDP Protocol = "udp" // udp
|
||||
ProtocolTCPUDP Protocol = "tcp/udp" // tcp/udp
|
||||
)
|
||||
|
||||
type Strategy string
|
||||
|
||||
var (
|
||||
StrategyAccept Strategy = "accept" // 接受
|
||||
StrategyDrop Strategy = "drop" // 丢弃
|
||||
StrategyReject Strategy = "reject" // 拒绝
|
||||
)
|
||||
|
||||
type Direction string
|
||||
|
||||
var (
|
||||
DirectionIn Direction = "in" // 传入
|
||||
DirectionOut Direction = "out" // 传出
|
||||
)
|
||||
|
||||
type FireInfo struct {
|
||||
Family string `json:"family"` // ipv4 ipv6
|
||||
Address string `json:"address"`
|
||||
Port uint `json:"port"` // 1-65535
|
||||
Protocol string `json:"protocol"` // tcp udp tcp/udp
|
||||
Strategy string `json:"strategy"` // accept drop
|
||||
Family string `json:"family"` // ipv4 ipv6
|
||||
Address string `json:"address"` // 源地址或目标地址
|
||||
PortStart uint `json:"port_start"` // 1-65535
|
||||
PortEnd uint `json:"port_end"` // 1-65535
|
||||
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
|
||||
Strategy Strategy `json:"strategy"` // accept drop reject
|
||||
Direction Direction `json:"direction"` // in out 入站或出站
|
||||
}
|
||||
|
||||
Num string `json:"num"`
|
||||
TargetIP string `json:"targetIP"`
|
||||
TargetPort string `json:"targetPort"` // 1-65535
|
||||
|
||||
UsedStatus string `json:"usedStatus"`
|
||||
Description string `json:"description"`
|
||||
type FireForwardInfo struct {
|
||||
Port uint `json:"port"` // 1-65535
|
||||
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
|
||||
TargetIP string `json:"target_ip"` // 目标地址
|
||||
TargetPort uint `json:"target_port"` // 1-65535
|
||||
}
|
||||
|
||||
type Forward struct {
|
||||
Num string `json:"num"`
|
||||
Protocol string `json:"protocol"`
|
||||
Port uint `json:"port"` // 1-65535
|
||||
TargetIP string `json:"targetIP"`
|
||||
TargetPort uint `json:"targetPort"` // 1-65535
|
||||
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
|
||||
Port uint `json:"port"` // 1-65535
|
||||
TargetIP string `json:"target_ip"` // 目标地址
|
||||
TargetPort uint `json:"target_port"` // 1-65535
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package firewall
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -13,13 +15,6 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type Operation string
|
||||
|
||||
var (
|
||||
OperationAdd Operation = "add"
|
||||
OperationDel Operation = "remove"
|
||||
)
|
||||
|
||||
type Firewall struct {
|
||||
forwardListRegex *regexp.Regexp
|
||||
richRuleRegex *regexp.Regexp
|
||||
@@ -28,7 +23,7 @@ type Firewall struct {
|
||||
func NewFirewall() *Firewall {
|
||||
firewall := &Firewall{
|
||||
forwardListRegex: regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`),
|
||||
richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)" (?:source address="([^"]+)" )?(?:port port="([^"]+)" )?(?:protocol="([^"]+)" )?(accept|drop|reject)$`),
|
||||
richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)"(?: .*?(source|destination) address="([^"]+)")?(?: .*?port port="([^"]+)")?(?: .*?protocol(?: value)?="([^"]+)")?.*?(accept|drop|reject)$`),
|
||||
}
|
||||
|
||||
return firewall
|
||||
@@ -58,13 +53,23 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
|
||||
if len(port) == 0 {
|
||||
continue
|
||||
}
|
||||
var itemPort FireInfo
|
||||
var item FireInfo
|
||||
if strings.Contains(port, "/") {
|
||||
itemPort.Port = cast.ToUint(strings.Split(port, "/")[0])
|
||||
itemPort.Protocol = strings.Split(port, "/")[1]
|
||||
ruleItem := strings.Split(port, "/")
|
||||
portItem := strings.Split(ruleItem[0], "-")
|
||||
if len(portItem) > 1 {
|
||||
item.PortStart = cast.ToUint(portItem[0])
|
||||
item.PortEnd = cast.ToUint(portItem[1])
|
||||
} else {
|
||||
item.PortStart = cast.ToUint(ruleItem[0])
|
||||
item.PortEnd = cast.ToUint(ruleItem[0])
|
||||
}
|
||||
item.Protocol = Protocol(ruleItem[1])
|
||||
}
|
||||
itemPort.Strategy = "accept"
|
||||
data = append(data, itemPort)
|
||||
item.Family = "ipv4"
|
||||
item.Strategy = "accept"
|
||||
item.Direction = "in"
|
||||
data = append(data, item)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
@@ -73,7 +78,6 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data = append(data, rich...)
|
||||
}()
|
||||
|
||||
@@ -81,13 +85,13 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *Firewall) ListForward() ([]FireInfo, error) {
|
||||
func (r *Firewall) ListForward() ([]FireForwardInfo, error) {
|
||||
out, err := shell.Execf("firewall-cmd --zone=public --list-forward-ports")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data []FireInfo
|
||||
var data []FireForwardInfo
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
line = strings.TrimFunc(line, func(r rune) bool {
|
||||
return r <= 32
|
||||
@@ -100,11 +104,11 @@ func (r *Firewall) ListForward() ([]FireInfo, error) {
|
||||
if len(match[4]) == 0 {
|
||||
match[4] = "127.0.0.1"
|
||||
}
|
||||
data = append(data, FireInfo{
|
||||
data = append(data, FireForwardInfo{
|
||||
Port: cast.ToUint(match[1]),
|
||||
Protocol: match[2],
|
||||
Protocol: Protocol(match[2]),
|
||||
TargetIP: match[4],
|
||||
TargetPort: match[3],
|
||||
TargetPort: cast.ToUint(match[3]),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -124,44 +128,35 @@ func (r *Firewall) ListRichRule() ([]FireInfo, error) {
|
||||
if len(rule) == 0 {
|
||||
continue
|
||||
}
|
||||
if itemRule, err := r.parseRichRule(rule); err == nil {
|
||||
data = append(data, *itemRule)
|
||||
if richRules, err := r.parseRichRule(rule); err == nil {
|
||||
data = append(data, richRules)
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *Firewall) Port(port FireInfo, operation Operation) error {
|
||||
stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d/%s --permanent", operation, port.Port, port.Protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s port %d/%s failed, err: %s", operation, port.Port, port.Protocol, stdout)
|
||||
func (r *Firewall) Port(rule FireInfo, operation Operation) error {
|
||||
if rule.PortEnd == 0 {
|
||||
rule.PortEnd = rule.PortStart
|
||||
}
|
||||
if rule.PortStart > rule.PortEnd {
|
||||
return fmt.Errorf("invalid port range: %d-%d", rule.PortStart, rule.PortEnd)
|
||||
}
|
||||
// 不支持的切换使用rich rules
|
||||
if (rule.Family != "" && rule.Family != "ipv4") || rule.Direction != "in" || rule.Address != "" || rule.Strategy != "accept" {
|
||||
return r.RichRules(rule, operation)
|
||||
}
|
||||
|
||||
_, err = shell.Execf("firewall-cmd --reload")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
|
||||
families := strings.Split(rule.Family, "/") // ipv4 ipv6
|
||||
|
||||
for _, family := range families {
|
||||
var ruleStr strings.Builder
|
||||
ruleStr.WriteString(fmt.Sprintf(`rule family="%s" `, family))
|
||||
if len(rule.Address) != 0 {
|
||||
ruleStr.WriteString(fmt.Sprintf(`source address="%s" `, rule.Address))
|
||||
}
|
||||
if rule.Port != 0 {
|
||||
ruleStr.WriteString(fmt.Sprintf(`port port="%d" `, rule.Port))
|
||||
}
|
||||
if len(rule.Protocol) != 0 {
|
||||
ruleStr.WriteString(fmt.Sprintf(`protocol="%s" `, rule.Protocol))
|
||||
}
|
||||
|
||||
ruleStr.WriteString(rule.Strategy)
|
||||
_, err := shell.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleStr.String())
|
||||
// 未设置协议默认为tcp/udp
|
||||
if rule.Protocol == "" {
|
||||
rule.Protocol = ProtocolTCPUDP
|
||||
}
|
||||
protocols := strings.Split(string(rule.Protocol), "/")
|
||||
for protocol := range slices.Values(protocols) {
|
||||
stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d-%d/%s --permanent", operation, rule.PortStart, rule.PortEnd, protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s rich rules (%s) failed, err: %v", operation, ruleStr.String(), err)
|
||||
return fmt.Errorf("%s port %d-%d/%s failed, err: %s", operation, rule.PortStart, rule.PortEnd, protocol, stdout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,45 +164,116 @@ func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Firewall) PortForward(info Forward, operation Operation) error {
|
||||
func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
|
||||
protocols := strings.Split(string(rule.Protocol), "/")
|
||||
for protocol := range slices.Values(protocols) {
|
||||
var ruleBuilder strings.Builder
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`rule family="%s" `, rule.Family))
|
||||
|
||||
if len(rule.Address) != 0 {
|
||||
if rule.Direction == "in" {
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`source address="%s" `, rule.Address))
|
||||
} else if rule.Direction == "out" {
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`destination address="%s" `, rule.Address))
|
||||
} else if rule.Direction != "" {
|
||||
return fmt.Errorf("invalid direction: %s", rule.Direction)
|
||||
}
|
||||
}
|
||||
if rule.PortStart != 0 && rule.PortEnd != 0 && (rule.PortStart != 1 && rule.PortEnd != 65535) { // 1-65535是解析出来无端口规则的情况
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`port port="%d-%d" `, rule.PortStart, rule.PortEnd))
|
||||
}
|
||||
if operation == OperationRemove && protocol != "" && rule.Protocol != "tcp/udp" { // 删除操作,可以不指定协议
|
||||
ruleBuilder.WriteString(`protocol`)
|
||||
if rule.PortStart == 0 && rule.PortEnd == 0 { // IP 规则下,必须添加 value
|
||||
ruleBuilder.WriteString(` value`)
|
||||
}
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`="%s" `, protocol))
|
||||
}
|
||||
if operation == OperationAdd && protocol != "" {
|
||||
ruleBuilder.WriteString(`protocol`)
|
||||
if rule.PortStart == 0 && rule.PortEnd == 0 { // IP 规则下,必须添加 value
|
||||
ruleBuilder.WriteString(` value`)
|
||||
}
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`="%s" `, protocol))
|
||||
}
|
||||
|
||||
ruleBuilder.WriteString(string(rule.Strategy))
|
||||
_, err := shell.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleBuilder.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s rich rules (%s) failed, err: %v", operation, ruleBuilder.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := shell.Execf("firewall-cmd --reload")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Firewall) Forward(rule Forward, operation Operation) error {
|
||||
if err := r.enableForward(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ruleStr strings.Builder
|
||||
ruleStr.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, info.Port, info.Protocol))
|
||||
if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" {
|
||||
ruleStr.WriteString(fmt.Sprintf("toaddr=%s:toport=%d", info.TargetIP, info.TargetPort))
|
||||
} else {
|
||||
ruleStr.WriteString(fmt.Sprintf("toport=%d", info.TargetPort))
|
||||
}
|
||||
ruleStr.WriteString(" --permanent")
|
||||
protocols := strings.Split(string(rule.Protocol), "/")
|
||||
for protocol := range slices.Values(protocols) {
|
||||
var ruleBuilder strings.Builder
|
||||
ruleBuilder.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, rule.Port, protocol))
|
||||
if rule.TargetIP != "" && !r.isLocalAddress(rule.TargetIP) {
|
||||
ruleBuilder.WriteString(fmt.Sprintf("toport=%d:toaddr=%s", rule.TargetPort, rule.TargetIP))
|
||||
} else {
|
||||
ruleBuilder.WriteString(fmt.Sprintf("toport=%d", rule.TargetPort))
|
||||
}
|
||||
ruleBuilder.WriteString(" --permanent")
|
||||
|
||||
_, err := shell.Execf(ruleStr.String()) // nolint: govet
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s port forward failed, err: %v", operation, err)
|
||||
_, err := shell.Execf(ruleBuilder.String()) // nolint: govet
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s port forward failed, err: %v", operation, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = shell.Execf("firewall-cmd --reload")
|
||||
_, err := shell.Execf("firewall-cmd --reload")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Firewall) parseRichRule(line string) (*FireInfo, error) {
|
||||
itemRule := new(FireInfo)
|
||||
if r.richRuleRegex.MatchString(line) {
|
||||
match := r.richRuleRegex.FindStringSubmatch(line)
|
||||
if len(match) < 6 {
|
||||
return nil, errors.New("invalid rich rule")
|
||||
}
|
||||
|
||||
itemRule.Family = match[1]
|
||||
itemRule.Address = match[2]
|
||||
itemRule.Port = cast.ToUint(match[3])
|
||||
itemRule.Protocol = match[4]
|
||||
itemRule.Strategy = match[5]
|
||||
func (r *Firewall) parseRichRule(line string) (FireInfo, error) {
|
||||
if !r.richRuleRegex.MatchString(line) {
|
||||
return FireInfo{}, errors.New("invalid rich rule format")
|
||||
}
|
||||
|
||||
return itemRule, nil
|
||||
match := r.richRuleRegex.FindStringSubmatch(line)
|
||||
if len(match) < 7 {
|
||||
return FireInfo{}, errors.New("invalid rich rule")
|
||||
}
|
||||
|
||||
fireInfo := FireInfo{
|
||||
Family: match[1],
|
||||
Address: match[3],
|
||||
Protocol: Protocol(match[5]),
|
||||
Strategy: Strategy(match[6]),
|
||||
}
|
||||
|
||||
if match[2] == "destination" {
|
||||
fireInfo.Direction = "out"
|
||||
} else {
|
||||
fireInfo.Direction = "in"
|
||||
}
|
||||
if fireInfo.Protocol == "" {
|
||||
fireInfo.Protocol = "tcp/udp"
|
||||
}
|
||||
|
||||
ports := strings.Split(match[4], "-")
|
||||
if len(ports) == 2 { // 添加端口范围
|
||||
fireInfo.PortStart = cast.ToUint(ports[0])
|
||||
fireInfo.PortEnd = cast.ToUint(ports[1])
|
||||
} else if len(ports) == 1 && ports[0] != "" { // 添加单个端口
|
||||
port := cast.ToUint(ports[0])
|
||||
fireInfo.PortStart = port
|
||||
fireInfo.PortEnd = port
|
||||
} else if len(ports) == 1 && ports[0] == "" { // 未添加端口规则,表示所有端口
|
||||
fireInfo.PortStart = 1
|
||||
fireInfo.PortEnd = 65535
|
||||
}
|
||||
|
||||
return fireInfo, nil
|
||||
}
|
||||
|
||||
func (r *Firewall) enableForward() error {
|
||||
@@ -216,15 +282,31 @@ func (r *Firewall) enableForward() error {
|
||||
if out == "no" {
|
||||
out, err = shell.Execf("firewall-cmd --zone=public --add-masquerade --permanent")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %s", err, out)
|
||||
return fmt.Errorf("%v: %s", err, out)
|
||||
}
|
||||
|
||||
_, err = shell.Execf("firewall-cmd --reload")
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("%v: %s", err, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Firewall) isLocalAddress(ip string) bool {
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil {
|
||||
return false
|
||||
}
|
||||
if parsed.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
if parsed.IsUnspecified() {
|
||||
return true
|
||||
}
|
||||
if strings.ToLower(ip) == "localhost" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func (s *NginxTestSuite) TestHTTPS() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.False(parser.GetHTTPS())
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/cert/default.pem", "/www/server/vhost/cert/default.key"))
|
||||
s.True(parser.GetHTTPS())
|
||||
expect, err := io.Read("testdata/https.conf")
|
||||
s.NoError(err)
|
||||
@@ -143,7 +143,7 @@ func (s *NginxTestSuite) TestHTTPS() {
|
||||
func (s *NginxTestSuite) TestHTTPSProtocols() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/cert/default.pem", "/www/server/vhost/cert/default.key"))
|
||||
s.Equal([]string{"TLSv1.2", "TLSv1.3"}, parser.GetHTTPSProtocols())
|
||||
s.NoError(parser.SetHTTPSProtocols([]string{"TLSv1.3"}))
|
||||
s.Equal([]string{"TLSv1.3"}, parser.GetHTTPSProtocols())
|
||||
@@ -152,7 +152,7 @@ func (s *NginxTestSuite) TestHTTPSProtocols() {
|
||||
func (s *NginxTestSuite) TestHTTPSCiphers() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/cert/default.pem", "/www/server/vhost/cert/default.key"))
|
||||
s.Equal("ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305", parser.GetHTTPSCiphers())
|
||||
s.NoError(parser.SetHTTPSCiphers("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"))
|
||||
s.Equal("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384", parser.GetHTTPSCiphers())
|
||||
@@ -162,7 +162,7 @@ func (s *NginxTestSuite) TestOCSP() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/cert/default.pem", "/www/server/vhost/cert/default.key"))
|
||||
s.False(parser.GetOCSP())
|
||||
s.NoError(parser.SetOCSP(false))
|
||||
s.False(parser.GetOCSP())
|
||||
@@ -175,7 +175,7 @@ func (s *NginxTestSuite) TestOCSP() {
|
||||
func (s *NginxTestSuite) TestHSTS() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/cert/default.pem", "/www/server/vhost/cert/default.key"))
|
||||
s.False(parser.GetHSTS())
|
||||
s.NoError(parser.SetHSTS(false))
|
||||
s.False(parser.GetHSTS())
|
||||
@@ -188,7 +188,7 @@ func (s *NginxTestSuite) TestHSTS() {
|
||||
func (s *NginxTestSuite) TestHTTPSRedirect() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/cert/default.pem", "/www/server/vhost/cert/default.key"))
|
||||
s.False(parser.GetHTTPSRedirect())
|
||||
s.NoError(parser.SetHTTPRedirect(false))
|
||||
s.False(parser.GetHTTPSRedirect())
|
||||
|
||||
@@ -3,8 +3,8 @@ server {
|
||||
server_name localhost;
|
||||
index index.php index.html index.htm;
|
||||
root /www/wwwroot/default;
|
||||
ssl_certificate /www/server/vhost/ssl/default.pem;
|
||||
ssl_certificate_key /www/server/vhost/ssl/default.key;
|
||||
ssl_certificate /www/server/vhost/cert/default.pem;
|
||||
ssl_certificate_key /www/server/vhost/cert/default.key;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
@@ -47,12 +47,12 @@ func Now(address ...string) (time.Time, error) {
|
||||
}
|
||||
|
||||
func UpdateSystemTime(time time.Time) error {
|
||||
_, err := shell.Execf(`sudo date -s "%s"`, time.Format("2006-01-02 15:04:05"))
|
||||
_, err := shell.Execf(`date -s '%s'`, time.Format("2006-01-02 15:04:05"))
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateSystemTimeZone(timezone string) error {
|
||||
_, err := shell.Execf(`sudo timedatectl set-timezone %s`, timezone)
|
||||
_, err := shell.Execf(`timedatectl set-timezone '%s'`, timezone)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,82 @@
|
||||
package os
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func readOSRelease() map[string]string {
|
||||
file, err := os.Open("/etc/os-release")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
osRelease := make(map[string]string)
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) > 0 && strings.Contains(line, "=") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
key := parts[0]
|
||||
value := strings.Trim(parts[1], `"`)
|
||||
osRelease[key] = value
|
||||
}
|
||||
}
|
||||
return osRelease
|
||||
}
|
||||
|
||||
// IsDebian 判断是否是 Debian 系统
|
||||
func IsDebian() bool {
|
||||
_, err := os.Stat("/etc/debian_version")
|
||||
return err == nil
|
||||
osRelease := readOSRelease()
|
||||
if osRelease == nil {
|
||||
return false
|
||||
}
|
||||
id, idLike := osRelease["ID"], osRelease["ID_LIKE"]
|
||||
return id == "debian" || strings.Contains(idLike, "debian")
|
||||
}
|
||||
|
||||
// IsRHEL 判断是否是 RHEL 系统
|
||||
func IsRHEL() bool {
|
||||
_, err := os.Stat("/etc/redhat-release")
|
||||
return err == nil
|
||||
osRelease := readOSRelease()
|
||||
if osRelease == nil {
|
||||
return false
|
||||
}
|
||||
// hce Huawei Cloud EulerOS
|
||||
// openEuler openEuler
|
||||
id, idLike := osRelease["ID"], osRelease["ID_LIKE"]
|
||||
return id == "tencentos" || id == "opencloudos" || id == "hce" || id == "openEuler" || id == "rhel" || strings.Contains(idLike, "rhel")
|
||||
}
|
||||
|
||||
// IsUbuntu 判断是否是 Ubuntu 系统
|
||||
func IsUbuntu() bool {
|
||||
_, err := os.Stat("/etc/lsb-release")
|
||||
return err == nil
|
||||
osRelease := readOSRelease()
|
||||
if osRelease == nil {
|
||||
return false
|
||||
}
|
||||
id, idLike := osRelease["ID"], osRelease["ID_LIKE"]
|
||||
return id == "ubuntu" || strings.Contains(idLike, "ubuntu")
|
||||
}
|
||||
|
||||
func TCPPortInUse(port uint) bool {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
conn, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
defer conn.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
func UDPPortInUse(port uint) bool {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
conn, err := net.ListenPacket("udp", addr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
defer conn.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -8,16 +8,10 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/slice"
|
||||
)
|
||||
|
||||
// Execf 执行 shell 命令
|
||||
func Execf(shell string, args ...any) (string, error) {
|
||||
if !CheckArgs(slice.ToString(args)...) {
|
||||
return "", errors.New("发现危险的命令参数,中止执行")
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
_ = os.Setenv("LC_ALL", "C")
|
||||
cmd = exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
|
||||
@@ -28,7 +22,7 @@ func Execf(shell string, args ...any) (string, error) {
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", errors.New(strings.TrimSpace(stderr.String()))
|
||||
return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), err
|
||||
@@ -36,10 +30,6 @@ func Execf(shell string, args ...any) (string, error) {
|
||||
|
||||
// ExecfAsync 异步执行 shell 命令
|
||||
func ExecfAsync(shell string, args ...any) error {
|
||||
if !CheckArgs(slice.ToString(args)...) {
|
||||
return errors.New("发现危险的命令参数,中止执行")
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
_ = os.Setenv("LC_ALL", "C")
|
||||
cmd = exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
|
||||
@@ -60,10 +50,6 @@ func ExecfAsync(shell string, args ...any) error {
|
||||
|
||||
// ExecfWithTimeout 执行 shell 命令并设置超时时间
|
||||
func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string, error) {
|
||||
if !CheckArgs(slice.ToString(args)...) {
|
||||
return "", errors.New("发现危险的命令参数,中止执行")
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
_ = os.Setenv("LC_ALL", "C")
|
||||
cmd = exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
|
||||
@@ -85,30 +71,22 @@ func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string,
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
_ = cmd.Process.Kill()
|
||||
return "", errors.New("执行超时")
|
||||
return strings.TrimSpace(stdout.String()), errors.New("执行超时")
|
||||
case err = <-done:
|
||||
if err != nil {
|
||||
return "", errors.New(strings.TrimSpace(stderr.String()))
|
||||
return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), err
|
||||
}
|
||||
|
||||
// CheckArgs 检查危险的参数
|
||||
func CheckArgs(args ...string) bool {
|
||||
if len(args) == 0 {
|
||||
return true
|
||||
}
|
||||
// ExecfWithOutput 执行 shell 命令并输出到终端
|
||||
func ExecfWithOutput(shell string, args ...any) error {
|
||||
_ = os.Setenv("LC_ALL", "C")
|
||||
cmd := exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
dangerous := []string{"&", "|", ";", "$", "'", `"`, "(", ")", "`", "\n", "\r", ">", "<", "{", "}", "[", "]", "\\"}
|
||||
for _, arg := range args {
|
||||
for _, char := range dangerous {
|
||||
if strings.Contains(arg, char) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ package tools
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/gookit/color"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
@@ -16,54 +16,71 @@ import (
|
||||
"github.com/shirou/gopsutil/net"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// MonitoringInfo 监控信息
|
||||
type MonitoringInfo struct {
|
||||
Cpus []cpu.InfoStat `json:"cpus"`
|
||||
Percent []float64 `json:"percent"`
|
||||
Load *load.AvgStat `json:"load"`
|
||||
Host *host.InfoStat `json:"host"`
|
||||
Mem *mem.VirtualMemoryStat `json:"mem"`
|
||||
Swap *mem.SwapMemoryStat `json:"swap"`
|
||||
Net []net.IOCountersStat `json:"net"`
|
||||
DiskIO []disk.IOCountersStat `json:"disk_io"`
|
||||
Disk []disk.PartitionStat `json:"disk"`
|
||||
DiskUsage []disk.UsageStat `json:"disk_usage"`
|
||||
}
|
||||
|
||||
// GetMonitoringInfo 获取监控数据
|
||||
func GetMonitoringInfo() MonitoringInfo {
|
||||
var res MonitoringInfo
|
||||
// CurrentInfo 获取监控数据
|
||||
func CurrentInfo(nets, disks []string) types.CurrentInfo {
|
||||
var res types.CurrentInfo
|
||||
res.Cpus, _ = cpu.Info()
|
||||
res.Percent, _ = cpu.Percent(time.Second, false)
|
||||
res.Percents, _ = cpu.Percent(100*time.Millisecond, true)
|
||||
percent, _ := cpu.Percent(100*time.Millisecond, false)
|
||||
if len(percent) > 0 {
|
||||
res.Percent = percent[0]
|
||||
}
|
||||
res.Load, _ = load.Avg()
|
||||
res.Host, _ = host.Info()
|
||||
res.Mem, _ = mem.VirtualMemory()
|
||||
res.Swap, _ = mem.SwapMemory()
|
||||
res.Net, _ = net.IOCounters(true)
|
||||
res.Disk, _ = disk.Partitions(true)
|
||||
|
||||
ioCounters, _ := disk.IOCounters()
|
||||
// 硬盘IO
|
||||
ioCounters, _ := disk.IOCounters(disks...)
|
||||
for _, info := range ioCounters {
|
||||
res.DiskIO = append(res.DiskIO, info)
|
||||
}
|
||||
|
||||
for _, partition := range res.Disk {
|
||||
if strings.HasPrefix(partition.Mountpoint, "/dev") || strings.HasPrefix(partition.Mountpoint, "/sys") || strings.HasPrefix(partition.Mountpoint, "/proc") || strings.HasPrefix(partition.Mountpoint, "/run") || strings.HasPrefix(partition.Mountpoint, "/boot") || strings.HasPrefix(partition.Mountpoint, "/usr") || strings.HasPrefix(partition.Mountpoint, "/var") {
|
||||
continue
|
||||
// 硬盘使用
|
||||
var excludes = []string{"/dev", "/boot", "/sys", "/dev", "/run", "/proc", "/usr", "/var", "/snap"}
|
||||
excludes = append(excludes, "/mnt/cdrom") // CDROM
|
||||
excludes = append(excludes, "/mnt/wsl") // Windows WSL
|
||||
res.Disk, _ = disk.Partitions(false)
|
||||
res.Disk = slices.DeleteFunc(res.Disk, func(d disk.PartitionStat) bool {
|
||||
for _, exclude := range excludes {
|
||||
if strings.HasPrefix(d.Mountpoint, exclude) {
|
||||
return true
|
||||
}
|
||||
// 去除内存盘和overlay容器盘
|
||||
if slices.Contains([]string{"tmpfs", "overlay"}, d.Fstype) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
// 分区使用
|
||||
for _, partition := range res.Disk {
|
||||
usage, _ := disk.Usage(partition.Mountpoint)
|
||||
res.DiskUsage = append(res.DiskUsage, *usage)
|
||||
}
|
||||
// 网络
|
||||
if len(nets) == 0 {
|
||||
netInfo, _ := net.IOCounters(false)
|
||||
res.Net = netInfo
|
||||
} else {
|
||||
var netStats []net.IOCountersStat
|
||||
netInfo, _ := net.IOCounters(true)
|
||||
for _, state := range netInfo {
|
||||
if slices.Contains(nets, state.Name) {
|
||||
netStats = append(netStats, state)
|
||||
}
|
||||
}
|
||||
res.Net = netStats
|
||||
}
|
||||
|
||||
res.Time = time.Now()
|
||||
return res
|
||||
}
|
||||
|
||||
// RestartPanel 重启面板
|
||||
func RestartPanel() {
|
||||
color.Greenln("重启面板...")
|
||||
_ = shell.ExecfAsync("sleep 1 && systemctl restart panel")
|
||||
color.Greenln("重启完成")
|
||||
}
|
||||
|
||||
// IsChina 是否中国大陆
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestHelperTestSuite(t *testing.T) {
|
||||
}
|
||||
|
||||
func (s *HelperTestSuite) TestGetMonitoringInfo() {
|
||||
s.NotNil(GetMonitoringInfo())
|
||||
s.NotNil(CurrentInfo(nil, nil))
|
||||
}
|
||||
|
||||
func (s *HelperTestSuite) TestGetPublicIP() {
|
||||
|
||||
@@ -10,6 +10,7 @@ type App struct {
|
||||
|
||||
// AppCenter 应用中心结构
|
||||
type AppCenter struct {
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type BackupFile struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size string `json:"size"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size string `json:"size"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package types
|
||||
|
||||
// 定义面板状态常量
|
||||
const (
|
||||
StatusNormal = iota
|
||||
StatusMaintain
|
||||
StatusClosed
|
||||
StatusUpgrade
|
||||
StatusFailed
|
||||
)
|
||||
|
||||
var Status = StatusNormal
|
||||
@@ -0,0 +1,28 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"github.com/shirou/gopsutil/load"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
"github.com/shirou/gopsutil/net"
|
||||
)
|
||||
|
||||
// CurrentInfo 监控信息
|
||||
type CurrentInfo struct {
|
||||
Cpus []cpu.InfoStat `json:"cpus"`
|
||||
Percent float64 `json:"percent"` // 总使用率
|
||||
Percents []float64 `json:"percents"` // 每个核心使用率
|
||||
Load *load.AvgStat `json:"load"`
|
||||
Host *host.InfoStat `json:"host"`
|
||||
Mem *mem.VirtualMemoryStat `json:"mem"`
|
||||
Swap *mem.SwapMemoryStat `json:"swap"`
|
||||
Net []net.IOCountersStat `json:"net"`
|
||||
DiskIO []disk.IOCountersStat `json:"disk_io"`
|
||||
Disk []disk.PartitionStat `json:"disk"`
|
||||
DiskUsage []disk.UsageStat `json:"disk_usage"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
/**
|
||||
* * 此处定义的是全局常量,启动或打包后将添加到window中
|
||||
* * 此处定义的是全局常量,启动或打包后将添加到 window 中
|
||||
* https://vitejs.cn/config/#define
|
||||
*/
|
||||
|
||||
// 项目构建时间
|
||||
const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'))
|
||||
const _BUILD_TIME_ = JSON.stringify(DateTime.now().toFormat('yyyy-MM-dd HH:mm:ss'))
|
||||
|
||||
export const viteDefine = {
|
||||
_BUILD_TIME_
|
||||
|
||||
@@ -26,11 +26,13 @@
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"install": "^0.13.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.5.0",
|
||||
"marked": "^14.1.2",
|
||||
"pinia": "^2.2.4",
|
||||
"pinia-plugin-persistedstate": "^4.1.1",
|
||||
"remove": "^0.1.5",
|
||||
"vue": "^3.5.11",
|
||||
"vue-echarts": "^7.0.3",
|
||||
@@ -38,12 +40,13 @@
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.258",
|
||||
"@iconify/json": "^2.2.260",
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"@rushstack/eslint-patch": "^1.10.4",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.16.11",
|
||||
"@unocss/eslint-config": "^0.63.4",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
@@ -52,13 +55,14 @@
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"colord": "^2.9.3",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"eslint-plugin-vue": "^9.29.0",
|
||||
"md-editor-v3": "^4.21.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"naive-ui": "^2.40.1",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"sass": "^1.79.4",
|
||||
"sass": "^1.79.5",
|
||||
"typescript": "^5.6.3",
|
||||
"unocss": "^0.63.4",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 运行评分
|
||||
test: (name: string, multi: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/benchmark/test', { name, multi })
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
add: (data: any): Promise<AxiosResponse<any>> => request.post('/apps/fail2ban/jails', data),
|
||||
// 删除保护
|
||||
delete: (name: string): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/apps/fail2ban/jails', { params: { name } }),
|
||||
request.delete('/apps/fail2ban/jails', { data: { name } }),
|
||||
// 封禁列表
|
||||
jail: (name: string): Promise<AxiosResponse<any>> => request.get('/apps/fail2ban/jails/' + name),
|
||||
// 解封 IP
|
||||
|
||||
@@ -37,5 +37,5 @@ export default {
|
||||
request.post(`/apps/php${version}/extensions`, { slug }),
|
||||
// 卸载拓展
|
||||
uninstallExtension: (version: number, slug: string): Promise<AxiosResponse<any>> =>
|
||||
request.delete(`/apps/php${version}/extensions`, { params: { slug } })
|
||||
request.delete(`/apps/php${version}/extensions`, { data: { slug } })
|
||||
}
|
||||
|
||||
@@ -19,19 +19,19 @@ export default {
|
||||
request.get('/apps/supervisor/processes', { params: { page, limit } }),
|
||||
// 进程启动
|
||||
startProcess: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/apps/supervisor/processes/${process}/start`, {}),
|
||||
request.post(`/apps/supervisor/processes/${process}/start`),
|
||||
// 进程停止
|
||||
stopProcess: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/apps/supervisor/processes/${process}/stop`, {}),
|
||||
request.post(`/apps/supervisor/processes/${process}/stop`),
|
||||
// 进程重启
|
||||
restartProcess: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/apps/supervisor/processes/${process}/restart`, {}),
|
||||
request.post(`/apps/supervisor/processes/${process}/restart`),
|
||||
// 进程日志
|
||||
processLog: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.get(`/apps/supervisor/processes/${process}/log`),
|
||||
// 清空进程日志
|
||||
clearProcessLog: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/apps/supervisor/processes/${process}/clearLog`, {}),
|
||||
request.post(`/apps/supervisor/processes/${process}/clearLog`),
|
||||
// 进程配置
|
||||
processConfig: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.get(`/apps/supervisor/processes/${process}`),
|
||||
|
||||
@@ -6,24 +6,34 @@ export default {
|
||||
// DNS
|
||||
dns: (): Promise<AxiosResponse<any>> => request.get('/apps/toolbox/dns'),
|
||||
// 设置 DNS
|
||||
setDns: (dns1: string, dns2: string): Promise<AxiosResponse<any>> =>
|
||||
updateDns: (dns1: string, dns2: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/toolbox/dns', { dns1, dns2 }),
|
||||
// SWAP
|
||||
swap: (): Promise<AxiosResponse<any>> => request.get('/apps/toolbox/swap'),
|
||||
// 设置 SWAP
|
||||
setSwap: (size: number): Promise<AxiosResponse<any>> =>
|
||||
updateSwap: (size: number): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/toolbox/swap', { size }),
|
||||
// 时区
|
||||
timezone: (): Promise<AxiosResponse<any>> => request.get('/apps/toolbox/timezone'),
|
||||
// 设置时区
|
||||
setTimezone: (timezone: string): Promise<AxiosResponse<any>> =>
|
||||
updateTimezone: (timezone: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/toolbox/timezone', { timezone }),
|
||||
// 设置时间
|
||||
updateTime: (time: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/toolbox/time', { time }),
|
||||
// 同步时间
|
||||
syncTime: (): Promise<AxiosResponse<any>> => request.post('/apps/toolbox/syncTime'),
|
||||
// 主机名
|
||||
hostname: (): Promise<AxiosResponse<any>> => request.get('/apps/toolbox/hostname'),
|
||||
// Hosts
|
||||
hosts: (): Promise<AxiosResponse<any>> => request.get('/apps/toolbox/hosts'),
|
||||
// 设置主机名
|
||||
updateHostname: (hostname: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/toolbox/hostname', { hostname }),
|
||||
// 设置 Hosts
|
||||
setHosts: (hosts: string): Promise<AxiosResponse<any>> =>
|
||||
updateHosts: (hosts: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/toolbox/hosts', { hosts }),
|
||||
// 设置 Root 密码
|
||||
setRootPassword: (password: string): Promise<AxiosResponse<any>> =>
|
||||
updateRootPassword: (password: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/toolbox/rootPassword', { password })
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
},
|
||||
// 删除备份
|
||||
delete: (type: string, file: string): Promise<AxiosResponse<any>> =>
|
||||
request.delete(`/backup/${type}/delete`, { params: { file } }),
|
||||
request.delete(`/backup/${type}/delete`, { data: { file } }),
|
||||
// 恢复备份
|
||||
restore: (type: string, file: string, target: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/backup/${type}/restore`, { file, target })
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
import type { AxiosResponse } from 'axios'
|
||||
import type { RequestConfig } from '~/types/axios'
|
||||
|
||||
export default {
|
||||
// 面板信息
|
||||
panel: (): Promise<Response> => fetch('/api/dashboard/panel'),
|
||||
// 面板菜单
|
||||
menu: (): Promise<AxiosResponse<any>> => request.get('/dashboard/menu'),
|
||||
// 首页应用
|
||||
homeApps: (): Promise<AxiosResponse<any>> => request.get('/dashboard/homeApps'),
|
||||
// 实时信息
|
||||
current: (nets: string[], disks: string[]): Promise<AxiosResponse<any>> =>
|
||||
request.post('/dashboard/current', { nets, disks }, { noNeedTip: true } as RequestConfig),
|
||||
// 系统信息
|
||||
systemInfo: (): Promise<AxiosResponse<any>> => request.get('/dashboard/systemInfo'),
|
||||
// 统计信息
|
||||
countInfo: (): Promise<AxiosResponse<any>> => request.get('/dashboard/countInfo'),
|
||||
// 已安装的数据库和PHP
|
||||
installedDbAndPhp: (): Promise<AxiosResponse<any>> => request.get('/dashboard/installedDbAndPhp'),
|
||||
// 检查更新
|
||||
checkUpdate: (): Promise<AxiosResponse<any>> => request.get('/dashboard/checkUpdate'),
|
||||
// 更新日志
|
||||
updateInfo: (): Promise<AxiosResponse<any>> => request.get('/dashboard/updateInfo'),
|
||||
// 更新面板
|
||||
update: (): Promise<AxiosResponse<any>> => request.post('/dashboard/update', null),
|
||||
// 重启面板
|
||||
restart: (): Promise<AxiosResponse<any>> => request.post('/dashboard/restart')
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取防火墙状态
|
||||
status: (): Promise<AxiosResponse<any>> => request.get('/firewall/status'),
|
||||
// 设置防火墙状态
|
||||
updateStatus: (status: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.post('/firewall/status', { status }),
|
||||
// 获取防火墙规则
|
||||
rules: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/firewall/rule', { params: { page, limit } }),
|
||||
// 创建防火墙规则
|
||||
createRule: (rule: any): Promise<AxiosResponse<any>> => request.post('/firewall/rule', rule),
|
||||
// 删除防火墙规则
|
||||
deleteRule: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/firewall/rule', { data: rule }),
|
||||
// 获取防火墙IP规则
|
||||
ipRules: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/firewall/ipRule', { params: { page, limit } }),
|
||||
// 创建防火墙IP规则
|
||||
createIpRule: (rule: any): Promise<AxiosResponse<any>> => request.post('/firewall/ipRule', rule),
|
||||
// 删除防火墙IP规则
|
||||
deleteIpRule: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/firewall/ipRule', { data: rule }),
|
||||
// 获取防火墙转发规则
|
||||
forwards: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/firewall/forward', { params: { page, limit } }),
|
||||
// 创建防火墙转发规则
|
||||
createForward: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.post('/firewall/forward', rule),
|
||||
// 删除防火墙转发规则
|
||||
deleteForward: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/firewall/forward', { data: rule })
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 面板信息
|
||||
panel: (): Promise<Response> => fetch('/api/info/panel'),
|
||||
// 面板菜单
|
||||
menu: (): Promise<AxiosResponse<any>> => request.get('/info/menu'),
|
||||
// 首页应用
|
||||
homeApps: (): Promise<AxiosResponse<any>> => request.get('/info/homeApps'),
|
||||
// 实时监控
|
||||
realtime: (): Promise<AxiosResponse<any>> => request.get('/info/realtime'),
|
||||
// 系统信息
|
||||
systemInfo: (): Promise<AxiosResponse<any>> => request.get('/info/systemInfo'),
|
||||
// 统计信息
|
||||
countInfo: (): Promise<AxiosResponse<any>> => request.get('/info/countInfo'),
|
||||
// 已安装的数据库和PHP
|
||||
installedDbAndPhp: (): Promise<AxiosResponse<any>> => request.get('/info/installedDbAndPhp'),
|
||||
// 检查更新
|
||||
checkUpdate: (): Promise<AxiosResponse<any>> => request.get('/info/checkUpdate'),
|
||||
// 更新日志
|
||||
updateInfo: (): Promise<AxiosResponse<any>> => request.get('/info/updateInfo'),
|
||||
// 更新面板
|
||||
update: (): Promise<AxiosResponse<any>> => request.post('/info/update', null),
|
||||
// 重启面板
|
||||
restart: (): Promise<AxiosResponse<any>> => request.post('/info/restart')
|
||||
}
|
||||
@@ -3,20 +3,6 @@ import type { AxiosResponse } from 'axios'
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取防火墙状态
|
||||
firewallStatus: (): Promise<AxiosResponse<any>> => request.get('/firewall/status'),
|
||||
// 设置防火墙状态
|
||||
setFirewallStatus: (status: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.post('/firewall/status', { status }),
|
||||
// 获取防火墙规则
|
||||
firewallRules: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/firewall/rule', { params: { page, limit } }),
|
||||
// 添加防火墙规则
|
||||
addFirewallRule: (port: number, protocol: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/firewall/rule', { port, protocol }),
|
||||
// 删除防火墙规则
|
||||
deleteFirewallRule: (port: number, protocol: string): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/firewall/rule', { data: { port, protocol } }),
|
||||
// 获取SSH
|
||||
ssh: (): Promise<AxiosResponse<any>> => request.get('/safe/ssh'),
|
||||
// 设置SSH
|
||||
|
||||