Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 577fb370a7 | |||
| 7ef8fc6787 | |||
| e5ba1375b1 | |||
| e8a01e2d04 | |||
| bdee27541c | |||
| f807da175d | |||
| ae29541d02 | |||
| 0f844ac3ea | |||
| 744d0b9617 | |||
| 1bea44146f | |||
| a7a68f6b34 |
@@ -68,7 +68,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 描述问题 (Describe The Problem)
|
label: 描述问题 (Describe The Problem)
|
||||||
description: |
|
description: |
|
||||||
简明概要地描述你遇到的问题。
|
简明概要地描述您遇到的问题。
|
||||||
Briefly describe the problem you are having.
|
Briefly describe the problem you are having.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -91,7 +91,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 预期行为 (Expected Behavior)
|
label: 预期行为 (Expected Behavior)
|
||||||
description: |
|
description: |
|
||||||
简明概要地描述你期望发生的事情。
|
简明概要地描述您期望发生的事情。
|
||||||
Briefly describe what you expect to happen.
|
Briefly describe what you expect to happen.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -113,7 +113,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 截图 (Screenshots)
|
label: 截图 (Screenshots)
|
||||||
description: |
|
description: |
|
||||||
如果有,添加屏幕截图可帮助更快定位你的问题。
|
如果有,添加屏幕截图可帮助更快定位您的问题。
|
||||||
If so, adding screenshots can help locate your issue faster.
|
If so, adding screenshots can help locate your issue faster.
|
||||||
可以复制图片后在此区域内粘贴以添加图片。
|
可以复制图片后在此区域内粘贴以添加图片。
|
||||||
Pictures can be copied and pasted in this area to add pictures.
|
Pictures can be copied and pasted in this area to add pictures.
|
||||||
@@ -128,7 +128,7 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
|
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
|
||||||
Environment? Browser? Software version? Related configuration? Link? References?
|
Environment? Browser? Software version? Related configuration? Link? References?
|
||||||
任何能让我们对你所遇到的问题有更多了解的东西。
|
任何能让我们对您所遇到的问题有更多了解的东西。
|
||||||
Anything that can give us more insight into the problem you're having.
|
Anything that can give us more insight into the problem you're having.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 描述功能 (Describe Feature)
|
label: 描述功能 (Describe Feature)
|
||||||
description: |
|
description: |
|
||||||
简明概要地描述你的新功能,以及它将解决什么问题。
|
简明概要地描述您的新功能,以及它将解决什么问题。
|
||||||
Briefly describe your new feature and what problem it will solve.
|
Briefly describe your new feature and what problem it will solve.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
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 |
@@ -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"
|
||||||
@@ -26,13 +26,13 @@
|
|||||||
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
|
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
|
||||||
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
|
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
|
||||||
4. **高效运维:** 面板界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
|
4. **高效运维:** 面板界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
|
||||||
5. **离线运行:** 面板运行可不依赖任何外部服务,你甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
5. **离线运行:** 面板运行可不依赖任何外部服务,您甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||||
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 2 年无事故。
|
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 2 年无事故。
|
||||||
7. **开源开放:** 面板开源,你可以自由修改、审计面板源码,安全性有保障。
|
7. **开源开放:** 面板开源,您可以自由修改、审计面板源码,安全性有保障。
|
||||||
|
|
||||||
## UI 截图
|
## UI 截图
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 运行环境
|
## 运行环境
|
||||||
|
|
||||||
@@ -65,9 +65,15 @@
|
|||||||
## 安装面板
|
## 安装面板
|
||||||
|
|
||||||
> **Warning**
|
> **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 为测试版本,欢迎帮助我们测试在不同操作系统下的兼容性。**
|
**当前 v2.3.x 为测试版本,欢迎帮助我们测试在不同操作系统下的兼容性。**
|
||||||
|
|
||||||
@@ -79,7 +85,7 @@ curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash in
|
|||||||
|
|
||||||
优先建议备份数据重装系统,这样可以保证系统纯净。
|
优先建议备份数据重装系统,这样可以保证系统纯净。
|
||||||
|
|
||||||
如果你无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
如果您无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
|
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
|
||||||
@@ -105,19 +111,35 @@ panel-cli
|
|||||||
|
|
||||||
## 赞助商
|
## 赞助商
|
||||||
|
|
||||||
如果耗子面板对你有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
|
如果耗子面板对您有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
|
||||||
|
|
||||||
**同时接受云资源赞助,可通过QQ群咨询联系**
|
**同时接受云资源赞助,可通过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/">
|
||||||
|
<img height="80" src=".github/assets/dk.png" alt="林枫云">
|
||||||
|
</a>
|
||||||
|
|
||||||
### CDN
|
### CDN
|
||||||
|
|
||||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||||
- [WAF PRO](https://waf.pro/)
|
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
|
||||||
- [盾云SCDN](https://scdn.ddunyun.com/)
|
</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">
|
<p align="center">
|
||||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
<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
|
## UI Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Operating Environment
|
## Operating Environment
|
||||||
|
|
||||||
@@ -67,7 +67,13 @@ As system versions are constantly updated, we may also terminate support for som
|
|||||||
> **Warning**
|
> **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.
|
> 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.**
|
**Current v2.3.x is a test version, welcome to help us test compatibility on different operating systems.**
|
||||||
|
|
||||||
@@ -111,13 +117,29 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
|
|||||||
|
|
||||||
### Financial
|
### 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/">
|
||||||
|
<img height="80" src=".github/assets/dk.png" alt="林枫云">
|
||||||
|
</a>
|
||||||
|
|
||||||
### CDN
|
### CDN
|
||||||
|
|
||||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||||
- [WAF PRO](https://waf.pro/)
|
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
|
||||||
- [盾云SCDN](https://scdn.ddunyun.com/)
|
</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">
|
<p align="center">
|
||||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ require (
|
|||||||
github.com/mholt/acmez/v2 v2.0.3
|
github.com/mholt/acmez/v2 v2.0.3
|
||||||
github.com/mholt/archiver/v4 v4.0.0-alpha.8
|
github.com/mholt/archiver/v4 v4.0.0-alpha.8
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
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/sethvargo/go-limiter v1.0.0
|
||||||
github.com/shirou/gopsutil v2.21.11+incompatible
|
github.com/shirou/gopsutil v2.21.11+incompatible
|
||||||
github.com/spf13/cast v1.7.0
|
github.com/spf13/cast v1.7.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha9
|
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8
|
||||||
|
github.com/urfave/cli/v3 v3.0.0-alpha9.1
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
golang.org/x/net v0.30.0
|
golang.org/x/net v0.30.0
|
||||||
@@ -108,7 +110,6 @@ require (
|
|||||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
go.opentelemetry.io/otel v1.30.0 // indirect
|
||||||
@@ -125,7 +126,6 @@ require (
|
|||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gotest.tools/v3 v3.5.1 // indirect
|
|
||||||
modernc.org/libc v1.60.1 // indirect
|
modernc.org/libc v1.60.1 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.8.0 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
|
|||||||
@@ -271,6 +271,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
|||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||||
|
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||||
|
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||||
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
|
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
|
||||||
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
||||||
github.com/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U=
|
github.com/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U=
|
||||||
@@ -297,15 +299,15 @@ 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/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 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||||
|
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=
|
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=
|
github.com/urfave/cli/v3 v3.0.0-alpha9.1 h1:1fJU+bltkwN8lF4Sni/X0i1d8XwPIrS82ivZ8qsp/q4=
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc=
|
github.com/urfave/cli/v3 v3.0.0-alpha9.1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
|||||||
@@ -25,9 +25,20 @@ var (
|
|||||||
Logger *zap.Logger
|
Logger *zap.Logger
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 定义面板状态常量
|
||||||
|
const (
|
||||||
|
StatusNormal = iota
|
||||||
|
StatusMaintain
|
||||||
|
StatusClosed
|
||||||
|
StatusUpgrade
|
||||||
|
StatusFailed
|
||||||
|
)
|
||||||
|
|
||||||
// 面板全局变量
|
// 面板全局变量
|
||||||
var (
|
var (
|
||||||
Root string
|
Root string
|
||||||
Version string
|
Version string
|
||||||
Locale string
|
Locale string
|
||||||
|
IsCli bool
|
||||||
|
Status = StatusNormal
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
package fail2ban
|
package fail2ban
|
||||||
|
|
||||||
type Add struct {
|
type Add struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name" validate:"required"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type" validate:"required"`
|
||||||
MaxRetry int `json:"maxretry"`
|
MaxRetry int `json:"maxretry" validate:"required"`
|
||||||
FindTime int `json:"findtime"`
|
FindTime int `json:"findtime" validate:"required"`
|
||||||
BanTime int `json:"bantime"`
|
BanTime int `json:"bantime" validate:"required"`
|
||||||
WebsiteName string `json:"website_name"`
|
WebsiteName string `json:"website_name"`
|
||||||
WebsiteMode string `json:"website_mode"`
|
WebsiteMode string `json:"website_mode"`
|
||||||
WebsitePath string `json:"website_path"`
|
WebsitePath string `json:"website_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Delete struct {
|
type Delete struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BanList struct {
|
type BanList struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Unban struct {
|
type Unban struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name" validate:"required"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SetWhiteList struct {
|
type SetWhiteList struct {
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package fail2ban
|
package fail2ban
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -111,10 +112,12 @@ func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var ports string
|
var ports string
|
||||||
for _, port := range website.Ports {
|
for _, listen := range website.Listens {
|
||||||
fields := strings.Fields(cast.ToString(port))
|
if port, err := cast.ToIntE(listen.Address); err == nil {
|
||||||
ports += fields[0] + ","
|
ports += fmt.Sprintf("%d", port) + ","
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
ports = strings.TrimSuffix(ports, ",")
|
||||||
|
|
||||||
rule := `
|
rule := `
|
||||||
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START
|
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package frp
|
package frp
|
||||||
|
|
||||||
type Name struct {
|
type Name struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package gitea
|
package gitea
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package mysql
|
package mysql
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SetRootPassword struct {
|
type SetRootPassword struct {
|
||||||
Password string `form:"password" json:"password"`
|
Password string `form:"password" json:"password" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
|
if err = io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
|
||||||
service.Error(w, http.StatusInternalServerError, "写入配置失败")
|
service.Error(w, http.StatusInternalServerError, "写入配置失败:%v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := systemctl.Reload("mysqld"); err != nil {
|
if err = systemctl.Restart("mysqld"); err != nil {
|
||||||
service.Error(w, http.StatusInternalServerError, "重载失败")
|
service.Error(w, http.StatusInternalServerError, "重启失败:%v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package nginx
|
package nginx
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package php
|
package php
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtensionSlug struct {
|
type ExtensionSlug struct {
|
||||||
Slug string `form:"slug" json:"slug"`
|
Slug string `form:"slug" json:"slug" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ func (s *Service) getExtensions() []Extension {
|
|||||||
{
|
{
|
||||||
Name: "exif",
|
Name: "exif",
|
||||||
Slug: "exif",
|
Slug: "exif",
|
||||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
Description: "通过 exif 扩展,您可以操作图像元数据。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "pdo_pgsql",
|
Name: "pdo_pgsql",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package phpmyadmin
|
package phpmyadmin
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePort struct {
|
type UpdatePort struct {
|
||||||
Port uint `form:"port" json:"port"`
|
Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,9 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
fw := firewall.NewFirewall()
|
fw := firewall.NewFirewall()
|
||||||
err = fw.Port(firewall.FireInfo{
|
err = fw.Port(firewall.FireInfo{
|
||||||
Port: req.Port,
|
PortStart: req.Port,
|
||||||
Protocol: "tcp",
|
PortEnd: req.Port,
|
||||||
|
Protocol: "tcp",
|
||||||
}, firewall.OperationAdd)
|
}, firewall.OperationAdd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package podman
|
package podman
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package postgresql
|
package postgresql
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
package pureftpd
|
package pureftpd
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Username string `form:"username" json:"username"`
|
Username string `form:"username" json:"username" validate:"required"`
|
||||||
Password string `form:"password" json:"password"`
|
Password string `form:"password" json:"password" validate:"required"`
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Delete struct {
|
type Delete struct {
|
||||||
Username string `form:"username" json:"username"`
|
Username string `form:"username" json:"username" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangePassword struct {
|
type ChangePassword struct {
|
||||||
Username string `form:"username" json:"username"`
|
Username string `form:"username" json:"username" validate:"required"`
|
||||||
Password string `form:"password" json:"password"`
|
Password string `form:"password" json:"password" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePort struct {
|
type UpdatePort struct {
|
||||||
Port uint `form:"port" json:"port"`
|
Port uint `form:"port" json:"port" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,8 +156,9 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
fw := firewall.NewFirewall()
|
fw := firewall.NewFirewall()
|
||||||
err = fw.Port(firewall.FireInfo{
|
err = fw.Port(firewall.FireInfo{
|
||||||
Port: req.Port,
|
PortStart: req.Port,
|
||||||
Protocol: "tcp",
|
PortEnd: req.Port,
|
||||||
|
Protocol: "tcp",
|
||||||
}, firewall.OperationAdd)
|
}, firewall.OperationAdd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package redis
|
package redis
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
package rsync
|
package rsync
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path" validate:"required"`
|
||||||
Comment string `form:"comment" json:"comment"`
|
Comment string `form:"comment" json:"comment"`
|
||||||
AuthUser string `form:"auth_user" json:"auth_user"`
|
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
|
||||||
Secret string `form:"secret" json:"secret"`
|
Secret string `form:"secret" json:"secret" validate:"required"`
|
||||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Delete struct {
|
type Delete struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path" validate:"required"`
|
||||||
Comment string `form:"comment" json:"comment"`
|
Comment string `form:"comment" json:"comment"`
|
||||||
AuthUser string `form:"auth_user" json:"auth_user"`
|
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
|
||||||
Secret string `form:"secret" json:"secret"`
|
Secret string `form:"secret" json:"secret" validate:"required"`
|
||||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package s3fs
|
package s3fs
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Ak string `form:"ak" json:"ak"`
|
Ak string `form:"ak" json:"ak" validate:"required"`
|
||||||
Sk string `form:"sk" json:"sk"`
|
Sk string `form:"sk" json:"sk" validate:"required"`
|
||||||
Bucket string `form:"bucket" json:"bucket"`
|
Bucket string `form:"bucket" json:"bucket" validate:"required"`
|
||||||
URL string `form:"url" json:"url"`
|
URL string `form:"url" json:"url" validate:"required"`
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Delete struct {
|
type Delete struct {
|
||||||
ID int64 `form:"id" json:"id"`
|
ID int64 `form:"id" json:"id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
package supervisor
|
package supervisor
|
||||||
|
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProcessConfig struct {
|
type UpdateProcessConfig struct {
|
||||||
Process string `form:"process" json:"process"`
|
Process string `form:"process" json:"process" validate:"required"`
|
||||||
Config string `form:"config" json:"config"`
|
Config string `form:"config" json:"config" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProcessName struct {
|
type ProcessName struct {
|
||||||
Process string `form:"process" json:"process"`
|
Process string `form:"process" json:"process" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateProcess struct {
|
type CreateProcess struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
User string `form:"user" json:"user"`
|
User string `form:"user" json:"user" validate:"required"`
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path" validate:"required"`
|
||||||
Command string `form:"command" json:"command"`
|
Command string `form:"command" json:"command" validate:"required"`
|
||||||
Num int `form:"num" json:"num"`
|
Num int `form:"num" json:"num" validate:"required,gte=1"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package toolbox
|
package toolbox
|
||||||
|
|
||||||
type DNS struct {
|
type DNS struct {
|
||||||
DNS1 string `form:"dns1" json:"dns1"`
|
DNS1 string `form:"dns1" json:"dns1" validate:"required"`
|
||||||
DNS2 string `form:"dns2" json:"dns2"`
|
DNS2 string `form:"dns2" json:"dns2" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SWAP struct {
|
type SWAP struct {
|
||||||
Size int64 `form:"size" json:"size"`
|
Size int64 `form:"size" json:"size" validate:"gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Timezone struct {
|
type Timezone struct {
|
||||||
Timezone string `form:"timezone" json:"timezone"`
|
Timezone string `form:"timezone" json:"timezone" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hosts struct {
|
type Hosts struct {
|
||||||
@@ -18,5 +18,5 @@ type Hosts struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Password struct {
|
type Password struct {
|
||||||
Password string `form:"password" json:"password"`
|
Password string `form:"password" json:"password" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TheTNB/panel/internal/http/request"
|
"github.com/TheTNB/panel/internal/http/request"
|
||||||
"github.com/TheTNB/panel/pkg/tools"
|
"github.com/TheTNB/panel/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Monitor struct {
|
type Monitor struct {
|
||||||
ID uint `gorm:"primaryKey" json:"id"`
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"`
|
Info types.CurrentInfo `gorm:"not null;serializer:json" json:"info"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitorRepo interface {
|
type MonitorRepo interface {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const (
|
|||||||
SettingKeySshPort SettingKey = "ssh_port"
|
SettingKeySshPort SettingKey = "ssh_port"
|
||||||
SettingKeySshUser SettingKey = "ssh_user"
|
SettingKeySshUser SettingKey = "ssh_user"
|
||||||
SettingKeySshPassword SettingKey = "ssh_password"
|
SettingKeySshPassword SettingKey = "ssh_password"
|
||||||
|
SettingKeyOfflineMode SettingKey = "offline_mode"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
@@ -33,9 +34,11 @@ type Setting struct {
|
|||||||
|
|
||||||
type SettingRepo interface {
|
type SettingRepo interface {
|
||||||
Get(key SettingKey, defaultValue ...string) (string, error)
|
Get(key SettingKey, defaultValue ...string) (string, error)
|
||||||
|
GetBool(key SettingKey, defaultValue ...bool) (bool, error)
|
||||||
Set(key SettingKey, value string) error
|
Set(key SettingKey, value string) error
|
||||||
Delete(key SettingKey) error
|
Delete(key SettingKey) error
|
||||||
GetPanelSetting(ctx context.Context) (*request.PanelSetting, error)
|
GetPanelSetting(ctx context.Context) (*request.PanelSetting, error)
|
||||||
UpdatePanelSetting(ctx context.Context, setting *request.PanelSetting) (bool, error)
|
UpdatePanelSetting(ctx context.Context, setting *request.PanelSetting) (bool, error)
|
||||||
UpdatePanel(version, url, checksum string) error
|
UpdatePanel(version, url, checksum string) error
|
||||||
|
FixPanel() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,5 @@ type TaskRepo interface {
|
|||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
UpdateStatus(id uint, status TaskStatus) error
|
UpdateStatus(id uint, status TaskStatus) error
|
||||||
Push(task *Task) error
|
Push(task *Task) error
|
||||||
DispatchWaiting() error
|
DispatchWaiting()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ type Website struct {
|
|||||||
Name string `gorm:"not null;unique" json:"name"`
|
Name string `gorm:"not null;unique" json:"name"`
|
||||||
Status bool `gorm:"not null;default:true" json:"status"`
|
Status bool `gorm:"not null;default:true" json:"status"`
|
||||||
Path string `gorm:"not null" json:"path"`
|
Path string `gorm:"not null" json:"path"`
|
||||||
PHP int `gorm:"not null" json:"php"`
|
Https bool `gorm:"not null" json:"https"`
|
||||||
SSL bool `gorm:"not null" json:"ssl"`
|
|
||||||
Remark string `gorm:"not null" json:"remark"`
|
Remark string `gorm:"not null" json:"remark"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ func BootWeb() {
|
|||||||
boot()
|
boot()
|
||||||
initValidator()
|
initValidator()
|
||||||
initSession()
|
initSession()
|
||||||
initCron()
|
|
||||||
initQueue()
|
initQueue()
|
||||||
|
initCron()
|
||||||
initHttp()
|
initHttp()
|
||||||
|
|
||||||
select {}
|
select {}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func initCli() {
|
func initCli() {
|
||||||
|
app.IsCli = true
|
||||||
|
|
||||||
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", "名称")
|
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", "名称")
|
||||||
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", "用法")
|
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", "用法")
|
||||||
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", "版本")
|
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", "版本")
|
||||||
@@ -43,6 +46,6 @@ func initCli() {
|
|||||||
Commands: route.Cli(),
|
Commands: route.Cli(),
|
||||||
}
|
}
|
||||||
if err := cmd.Run(context.Background(), os.Args); err != nil {
|
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() {
|
func initGlobal() {
|
||||||
app.Root = app.Conf.MustString("app.root")
|
app.Root = app.Conf.MustString("app.root")
|
||||||
app.Version = "2.3.3"
|
app.Version = "2.3.7"
|
||||||
app.Locale = app.Conf.MustString("app.locale")
|
app.Locale = app.Conf.MustString("app.locale")
|
||||||
|
|
||||||
// 初始化时区
|
// 初始化时区
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/go-playground/validator/v10/translations/zh"
|
"github.com/go-playground/validator/v10/translations/zh"
|
||||||
|
|
||||||
"github.com/TheTNB/panel/internal/app"
|
"github.com/TheTNB/panel/internal/app"
|
||||||
|
"github.com/TheTNB/panel/internal/http/rule"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initValidator() {
|
func initValidator() {
|
||||||
@@ -23,4 +24,7 @@ func initValidator() {
|
|||||||
|
|
||||||
app.Translator = &trans
|
app.Translator = &trans
|
||||||
app.Validator = validate
|
app.Validator = validate
|
||||||
|
if err := rule.RegisterRules(validate); err != nil {
|
||||||
|
log.Fatalf("failed to register validator rules: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ func (r *appRepo) Update(slug string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
task := new(biz.Task)
|
task := new(biz.Task)
|
||||||
task.Name = "更新应用 " + item.Name
|
task.Name = "升级应用 " + item.Name
|
||||||
task.Status = biz.TaskStatusWaiting
|
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.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"
|
task.Log = "/tmp/" + item.Slug + ".log"
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
|
|||||||
Name: file.Name(),
|
Name: file.Name(),
|
||||||
Path: filepath.Join(path, file.Name()),
|
Path: filepath.Join(path, file.Name()),
|
||||||
Size: str.FormatBytes(float64(info.Size())),
|
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:]
|
toDelete := filtered[save:]
|
||||||
for _, file := range toDelete {
|
for _, file := range toDelete {
|
||||||
filePath := filepath.Join(path, file.Name())
|
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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
if app.IsCli {
|
||||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
|
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||||
|
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,8 +289,10 @@ func (r *backupRepo) createMySQL(to string, name string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
if app.IsCli {
|
||||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
|
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||||
|
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,8 +326,10 @@ func (r *backupRepo) createPostgres(to string, name string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
if app.IsCli {
|
||||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
|
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||||
|
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,9 +349,14 @@ func (r *backupRepo) createPanel(to string) error {
|
|||||||
}, backup, io.Zip); err != nil {
|
}, backup, io.Zip); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := io.Chmod(backup, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
if app.IsCli {
|
||||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
|
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||||
|
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,10 +477,12 @@ func (r *backupRepo) preCheckPath(to, path string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
if app.IsCli {
|
||||||
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
|
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
|
||||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||||
|
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||||
|
}
|
||||||
|
|
||||||
if uint64(size) > usage.Free {
|
if uint64(size) > usage.Free {
|
||||||
return errors.New("备份目录空间不足")
|
return errors.New("备份目录空间不足")
|
||||||
@@ -483,9 +503,11 @@ func (r *backupRepo) preCheckDB(to string, size int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
if app.IsCli {
|
||||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||||
|
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||||
|
}
|
||||||
|
|
||||||
if uint64(size) > usage.Free {
|
if uint64(size) > usage.Free {
|
||||||
return errors.New("备份目录空间不足")
|
return errors.New("备份目录空间不足")
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,7 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
|
|||||||
return nil, errors.New("请重新获取 DNS 解析记录")
|
return nil, errors.New("请重新获取 DNS 解析记录")
|
||||||
}
|
}
|
||||||
|
|
||||||
ssl, err := r.client.ObtainSSLManual()
|
ssl, err := r.client.ObtainCertificateManual()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -236,10 +236,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
|||||||
return err
|
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
|
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
|
return err
|
||||||
}
|
}
|
||||||
if err = systemctl.Reload("nginx"); err != nil {
|
if err = systemctl.Reload("nginx"); err != nil {
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
|||||||
|
|
||||||
# 耗子面板 - 网站备份脚本
|
# 耗子面板 - 网站备份脚本
|
||||||
|
|
||||||
panel-cli backup website -n %s -p %s
|
panel-cli backup website -n '%s' -p '%s'
|
||||||
panel-cli backup clear -t website -f %s -s %d -p %s
|
panel-cli backup clear -t website -f '%s' -s '%d' -p '%s'
|
||||||
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
|
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
|
||||||
}
|
}
|
||||||
if req.BackupType == "mysql" || req.BackupType == "postgres" {
|
if req.BackupType == "mysql" || req.BackupType == "postgres" {
|
||||||
@@ -76,8 +76,8 @@ export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
|||||||
|
|
||||||
# 耗子面板 - 数据库备份脚本
|
# 耗子面板 - 数据库备份脚本
|
||||||
|
|
||||||
panel-cli backup database -t %s -n %s -p %s
|
panel-cli backup database -t '%s' -n '%s' -p '%s'
|
||||||
panel-cli backup clear -t %s -f %s -s %d -p %s
|
panel-cli backup clear -t '%s' -f '%s' -s '%d' -p '%s'
|
||||||
`, req.BackupType, req.Target, req.BackupPath, req.BackupType, req.Target, req.Save, req.BackupPath)
|
`, req.BackupType, req.Target, req.BackupPath, req.BackupType, req.Target, req.Save, req.BackupPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r monitorRepo) Clear() 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) {
|
func (r monitorRepo) List(start, end time.Time) ([]*biz.Monitor, error) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"slices"
|
||||||
|
|
||||||
"github.com/go-rat/utils/hash"
|
"github.com/go-rat/utils/hash"
|
||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
@@ -16,16 +16,21 @@ import (
|
|||||||
"github.com/TheTNB/panel/internal/app"
|
"github.com/TheTNB/panel/internal/app"
|
||||||
"github.com/TheTNB/panel/internal/biz"
|
"github.com/TheTNB/panel/internal/biz"
|
||||||
"github.com/TheTNB/panel/internal/http/request"
|
"github.com/TheTNB/panel/internal/http/request"
|
||||||
|
"github.com/TheTNB/panel/pkg/firewall"
|
||||||
"github.com/TheTNB/panel/pkg/io"
|
"github.com/TheTNB/panel/pkg/io"
|
||||||
"github.com/TheTNB/panel/pkg/shell"
|
"github.com/TheTNB/panel/pkg/shell"
|
||||||
"github.com/TheTNB/panel/pkg/tools"
|
"github.com/TheTNB/panel/pkg/tools"
|
||||||
"github.com/TheTNB/panel/pkg/types"
|
"github.com/TheTNB/panel/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type settingRepo struct{}
|
type settingRepo struct {
|
||||||
|
taskRepo biz.TaskRepo
|
||||||
|
}
|
||||||
|
|
||||||
func NewSettingRepo() biz.SettingRepo {
|
func NewSettingRepo() biz.SettingRepo {
|
||||||
return &settingRepo{}
|
return &settingRepo{
|
||||||
|
taskRepo: NewTaskRepo(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) {
|
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
|
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 {
|
func (r *settingRepo) Set(key biz.SettingKey, value string) error {
|
||||||
setting := new(biz.Setting)
|
setting := new(biz.Setting)
|
||||||
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
|
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) {
|
func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSetting, error) {
|
||||||
name := new(biz.Setting)
|
name, err := r.Get(biz.SettingKeyName)
|
||||||
if err := app.Orm.Where("key = ?", biz.SettingKeyName).First(name).Error; err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
websitePath := new(biz.Setting)
|
offlineMode, err := r.Get(biz.SettingKeyOfflineMode)
|
||||||
if err := app.Orm.Where("key = ?", biz.SettingKeyWebsitePath).First(websitePath).Error; err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
backupPath := new(biz.Setting)
|
websitePath, err := r.Get(biz.SettingKeyWebsitePath)
|
||||||
if err := app.Orm.Where("key = ?", biz.SettingKeyBackupPath).First(backupPath).Error; err != nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
backupPath, err := r.Get(biz.SettingKeyBackupPath)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +119,12 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &request.PanelSetting{
|
return &request.PanelSetting{
|
||||||
Name: name.Value,
|
Name: name,
|
||||||
Locale: app.Conf.String("app.locale"),
|
Locale: app.Conf.String("app.locale"),
|
||||||
Entrance: app.Conf.String("http.entrance"),
|
Entrance: app.Conf.String("http.entrance"),
|
||||||
WebsitePath: websitePath.Value,
|
OfflineMode: cast.ToBool(offlineMode),
|
||||||
BackupPath: backupPath.Value,
|
WebsitePath: websitePath,
|
||||||
|
BackupPath: backupPath,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Port: app.Conf.Int("http.port"),
|
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 {
|
if err := r.Set(biz.SettingKeyName, setting.Name); err != nil {
|
||||||
return false, err
|
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 {
|
if err := r.Set(biz.SettingKeyWebsitePath, setting.WebsitePath); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -120,6 +148,37 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
|||||||
return false, err
|
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 {
|
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -127,7 +186,7 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
restartFlag := false
|
// 面板主配置
|
||||||
config := new(types.PanelConfig)
|
config := new(types.PanelConfig)
|
||||||
cm := yaml.CommentMap{}
|
cm := yaml.CommentMap{}
|
||||||
raw, err := io.Read("/usr/local/etc/panel/config.yml")
|
raw, err := io.Read("/usr/local/etc/panel/config.yml")
|
||||||
@@ -143,33 +202,28 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
|||||||
config.HTTP.Entrance = setting.Entrance
|
config.HTTP.Entrance = setting.Entrance
|
||||||
config.HTTP.TLS = setting.HTTPS
|
config.HTTP.TLS = setting.HTTPS
|
||||||
|
|
||||||
|
// 放行端口
|
||||||
|
fw := firewall.NewFirewall()
|
||||||
|
err = fw.Port(firewall.FireInfo{
|
||||||
|
PortStart: uint(config.HTTP.Port),
|
||||||
|
PortEnd: uint(config.HTTP.Port),
|
||||||
|
Protocol: "tcp",
|
||||||
|
}, firewall.OperationAdd)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
encoded, err := yaml.MarshalWithOptions(config, yaml.WithComment(cm))
|
encoded, err := yaml.MarshalWithOptions(config, yaml.WithComment(cm))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
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 raw != string(encoded) {
|
||||||
|
if r.taskRepo.HasRunningTask() {
|
||||||
|
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
|
||||||
|
}
|
||||||
restartFlag = true
|
restartFlag = true
|
||||||
}
|
}
|
||||||
|
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
|
||||||
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,108 +231,128 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
||||||
name := filepath.Base(url)
|
// 预先优化数据库
|
||||||
color.Greenln("目标版本: ", version)
|
if err := app.Orm.Exec("VACUUM").Error; err != nil {
|
||||||
color.Greenln("下载链接: ", url)
|
return err
|
||||||
color.Greenln("文件名: ", name)
|
}
|
||||||
|
if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
|
||||||
color.Greenln("前置检查...")
|
return err
|
||||||
if io.Exists("/tmp/panel-storage.zip") {
|
|
||||||
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败导致的,请排除后重试")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Greenln("备份面板数据...")
|
name := filepath.Base(url)
|
||||||
|
if app.IsCli {
|
||||||
|
color.Greenln(fmt.Sprintf("|-目标版本:%s", version))
|
||||||
|
color.Greenln(fmt.Sprintf("|-下载链接:%s", url))
|
||||||
|
color.Greenln(fmt.Sprintf("|-文件名:%s", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
backup := NewBackupRepo()
|
||||||
color.Redln("备份面板失败:", err)
|
if err := backup.Create(biz.BackupTypePanel, ""); err != nil {
|
||||||
return err
|
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 {
|
if err := io.Compress([]string{filepath.Join(app.Root, "panel/storage")}, "/tmp/panel-storage.zip", io.Zip); err != nil {
|
||||||
color.Redln("备份面板数据失败:", err)
|
if app.IsCli {
|
||||||
return err
|
color.Redln("|-备份面板数据失败:", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("备份面板数据失败:%w", err)
|
||||||
}
|
}
|
||||||
if !io.Exists("/tmp/panel-storage.zip") {
|
if !io.Exists("/tmp/panel-storage.zip") {
|
||||||
return errors.New("已备份面板数据检查失败")
|
return errors.New("已备份面板数据检查失败")
|
||||||
}
|
}
|
||||||
color.Greenln("备份完成")
|
|
||||||
|
|
||||||
color.Greenln("清理旧版本...")
|
if app.IsCli {
|
||||||
|
color.Greenln("|-清理旧版本...")
|
||||||
|
}
|
||||||
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
|
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
|
||||||
color.Redln("清理旧版本失败:", err)
|
return fmt.Errorf("清理旧版本失败:%w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
color.Greenln("清理完成")
|
|
||||||
|
|
||||||
color.Greenln("正在下载...")
|
if app.IsCli {
|
||||||
if _, err := shell.Execf("wget -T 120 -t 3 -O %s/panel/%s %s", app.Root, name, url); err != nil {
|
color.Greenln("|-解压新版本...")
|
||||||
color.Redln("下载失败:", err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if _, err := shell.Execf("wget -T 20 -t 3 -O %s/panel/%s %s", app.Root, name+".sha256", checksum); err != nil {
|
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel"), io.Zip); err != nil {
|
||||||
color.Redln("下载失败:", err)
|
return fmt.Errorf("解压失败:%w", 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 !io.Exists(filepath.Join(app.Root, "panel", "web")) {
|
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
|
||||||
return errors.New("更新失败,可能是下载过程中出现了问题")
|
return errors.New("解压失败,缺失文件")
|
||||||
}
|
}
|
||||||
color.Greenln("更新完成")
|
|
||||||
|
|
||||||
color.Greenln("恢复面板数据...")
|
if app.IsCli {
|
||||||
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
|
color.Greenln("|-恢复面板数据...")
|
||||||
color.Redln("恢复面板数据失败:", err)
|
}
|
||||||
return err
|
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")) {
|
if !io.Exists(filepath.Join(app.Root, "panel/storage/app.db")) {
|
||||||
return errors.New("恢复面板数据失败")
|
return errors.New("恢复面板数据失败")
|
||||||
}
|
}
|
||||||
color.Greenln("恢复完成")
|
|
||||||
|
|
||||||
color.Greenln("运行升级后脚本...")
|
if app.IsCli {
|
||||||
if _, err = shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
|
color.Greenln("|-运行升级后脚本...")
|
||||||
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 {
|
if _, err := shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
|
||||||
color.Redln("下载面板服务文件失败:", err)
|
return fmt.Errorf("运行面板升级后脚本失败:%w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if _, err = shell.Execf("panel-cli setting write version %s", version); err != nil {
|
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 fmt.Errorf("下载面板服务文件失败:%w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if err = io.Mv(filepath.Join(app.Root, "panel/cli"), "/usr/local/sbin/panel-cli"); err != nil {
|
if _, err := shell.Execf("panel-cli setting write version %s", version); err != nil {
|
||||||
color.Redln("移动面板命令行工具失败:", err)
|
return fmt.Errorf("写入面板版本号失败:%w", err)
|
||||||
return 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("/usr/local/sbin/panel-cli", 0700)
|
||||||
_ = io.Chmod("/etc/systemd/system/panel.service", 0700)
|
_ = io.Chmod("/etc/systemd/system/panel.service", 0700)
|
||||||
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
|
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
|
||||||
color.Greenln("设置完成")
|
|
||||||
|
|
||||||
color.Greenln("升级完成")
|
if app.IsCli {
|
||||||
|
color.Greenln("|-升级完成")
|
||||||
|
}
|
||||||
|
|
||||||
_, _ = shell.Execf("systemctl daemon-reload")
|
_, _ = shell.Execf("systemctl daemon-reload")
|
||||||
_ = io.Remove("/tmp/panel-storage.zip")
|
_ = io.Remove("/tmp/panel-storage.zip")
|
||||||
@@ -287,3 +361,134 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
|||||||
|
|
||||||
return nil
|
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
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/TheTNB/panel/internal/app"
|
"github.com/TheTNB/panel/internal/app"
|
||||||
"github.com/TheTNB/panel/internal/biz"
|
"github.com/TheTNB/panel/internal/biz"
|
||||||
"github.com/TheTNB/panel/internal/queuejob"
|
"github.com/TheTNB/panel/internal/queuejob"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var taskDispatchOnce sync.Once
|
||||||
|
|
||||||
type taskRepo struct{}
|
type taskRepo struct{}
|
||||||
|
|
||||||
func NewTaskRepo() biz.TaskRepo {
|
func NewTaskRepo() biz.TaskRepo {
|
||||||
|
task := &taskRepo{}
|
||||||
|
taskDispatchOnce.Do(task.DispatchWaiting)
|
||||||
return &taskRepo{}
|
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
|
var tasks []biz.Task
|
||||||
if err := app.Orm.Where("status = ?", biz.TaskStatusWaiting).Find(&tasks).Error; err != nil {
|
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 {
|
for _, task := range tasks {
|
||||||
if err := r.Push(&task); err != nil {
|
if err := app.Queue.Push(queuejob.NewProcessTask(r), []any{
|
||||||
return err
|
task.ID,
|
||||||
|
}); err != nil {
|
||||||
|
app.Logger.Error("推送任务失败", zap.Error(err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cast"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/TheTNB/panel/internal/app"
|
"github.com/TheTNB/panel/internal/app"
|
||||||
"github.com/TheTNB/panel/internal/biz"
|
"github.com/TheTNB/panel/internal/biz"
|
||||||
@@ -18,8 +15,9 @@ import (
|
|||||||
"github.com/TheTNB/panel/pkg/cert"
|
"github.com/TheTNB/panel/pkg/cert"
|
||||||
"github.com/TheTNB/panel/pkg/db"
|
"github.com/TheTNB/panel/pkg/db"
|
||||||
"github.com/TheTNB/panel/pkg/io"
|
"github.com/TheTNB/panel/pkg/io"
|
||||||
|
"github.com/TheTNB/panel/pkg/nginx"
|
||||||
|
"github.com/TheTNB/panel/pkg/punycode"
|
||||||
"github.com/TheTNB/panel/pkg/shell"
|
"github.com/TheTNB/panel/pkg/shell"
|
||||||
"github.com/TheTNB/panel/pkg/str"
|
|
||||||
"github.com/TheTNB/panel/pkg/systemctl"
|
"github.com/TheTNB/panel/pkg/systemctl"
|
||||||
"github.com/TheTNB/panel/pkg/types"
|
"github.com/TheTNB/panel/pkg/types"
|
||||||
)
|
)
|
||||||
@@ -59,79 +57,84 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
|||||||
if err := app.Orm.Where("id", id).First(website).Error; err != nil {
|
if err := app.Orm.Where("id", id).First(website).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// 解析nginx配置
|
||||||
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
p, err := nginx.NewParser(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
setting := new(types.WebsiteSetting)
|
setting := new(types.WebsiteSetting)
|
||||||
setting.ID = website.ID
|
setting.ID = website.ID
|
||||||
setting.Name = website.Name
|
setting.Name = website.Name
|
||||||
setting.Path = website.Path
|
setting.Path = website.Path
|
||||||
setting.SSL = website.SSL
|
setting.HTTPS = website.Https
|
||||||
setting.PHP = website.PHP
|
setting.PHP = p.GetPHP()
|
||||||
setting.Raw = config
|
setting.Raw = config
|
||||||
|
// 监听地址
|
||||||
portStr := str.Cut(config, "# port标记位开始", "# port标记位结束")
|
listens, err := p.GetListen()
|
||||||
matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(portStr, -1)
|
if err != nil {
|
||||||
for _, match := range matches {
|
return nil, err
|
||||||
if len(match) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 跳过 ipv6
|
|
||||||
if strings.Contains(match[1], "[::]") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 443 ssl 之类的情况
|
|
||||||
ports := strings.Fields(match[1])
|
|
||||||
if len(ports) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !slices.Contains(setting.Ports, cast.ToUint(ports[0])) {
|
|
||||||
setting.Ports = append(setting.Ports, cast.ToUint(ports[0]))
|
|
||||||
}
|
|
||||||
if len(ports) > 1 && ports[1] == "ssl" {
|
|
||||||
setting.SSLPorts = append(setting.SSLPorts, cast.ToUint(ports[0]))
|
|
||||||
} else if len(ports) > 1 && ports[1] == "quic" {
|
|
||||||
setting.QUICPorts = append(setting.QUICPorts, cast.ToUint(ports[0]))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束")
|
setting.Listens = lo.Map(
|
||||||
match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName)
|
lo.UniqBy(listens, func(listen []string) string {
|
||||||
if len(match) > 1 {
|
if len(listen) == 0 {
|
||||||
setting.Domains = strings.Split(match[1], " ")
|
return ""
|
||||||
|
}
|
||||||
|
return listen[0]
|
||||||
|
}),
|
||||||
|
func(listen []string, _ int) types.WebsiteListen {
|
||||||
|
addr := listen[0]
|
||||||
|
grouped := lo.GroupBy(listens, func(listen []string) string {
|
||||||
|
if len(listen) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return listen[0]
|
||||||
|
})[addr]
|
||||||
|
return types.WebsiteListen{
|
||||||
|
Address: addr,
|
||||||
|
HTTPS: lo.SomeBy(grouped, func(listen []string) bool { return lo.Contains(listen, "ssl") }),
|
||||||
|
QUIC: lo.SomeBy(grouped, func(listen []string) bool { return lo.Contains(listen, "quic") }),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// 域名
|
||||||
|
domains, err := p.GetServerName()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
root := str.Cut(config, "# root标记位开始", "# root标记位结束")
|
domains, err = punycode.DecodeDomains(domains)
|
||||||
match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root)
|
if err != nil {
|
||||||
if len(match) > 1 {
|
return nil, err
|
||||||
setting.Root = match[1]
|
|
||||||
}
|
}
|
||||||
index := str.Cut(config, "# index标记位开始", "# index标记位结束")
|
setting.Domains = domains
|
||||||
match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index)
|
// 运行目录
|
||||||
if len(match) > 1 {
|
root, _ := p.GetRoot()
|
||||||
setting.Index = match[1]
|
setting.Root = root
|
||||||
}
|
// 默认文档
|
||||||
|
index, _ := p.GetIndex()
|
||||||
|
setting.Index = index
|
||||||
|
// 防跨站
|
||||||
if io.Exists(filepath.Join(setting.Root, ".user.ini")) {
|
if io.Exists(filepath.Join(setting.Root, ".user.ini")) {
|
||||||
userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini"))
|
userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini"))
|
||||||
if strings.Contains(userIni, "open_basedir") {
|
if strings.Contains(userIni, "open_basedir") {
|
||||||
setting.OpenBasedir = true
|
setting.OpenBasedir = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// HTTPS
|
||||||
crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"))
|
if setting.HTTPS {
|
||||||
setting.SSLCertificate = crt
|
setting.HTTPRedirect = p.GetHTTPSRedirect()
|
||||||
key, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"))
|
setting.HSTS = p.GetHSTS()
|
||||||
setting.SSLCertificateKey = key
|
setting.OCSP = p.GetOCSP()
|
||||||
if setting.SSL {
|
|
||||||
ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束")
|
|
||||||
setting.HTTPRedirect = strings.Contains(ssl, "# http重定向标记位")
|
|
||||||
setting.HSTS = strings.Contains(ssl, "# hsts标记位")
|
|
||||||
setting.OCSP = strings.Contains(ssl, "# ocsp标记位")
|
|
||||||
}
|
}
|
||||||
|
// 证书
|
||||||
|
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/cert", website.Name+".key"))
|
||||||
|
setting.SSLCertificateKey = key
|
||||||
// 解析证书信息
|
// 解析证书信息
|
||||||
if decode, err := cert.ParseCert(crt); err == nil {
|
if decode, err := cert.ParseCert(crt); err == nil {
|
||||||
setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05")
|
setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05")
|
||||||
@@ -140,9 +143,10 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
|||||||
setting.SSLOCSPServer = decode.OCSPServer
|
setting.SSLOCSPServer = decode.OCSPServer
|
||||||
setting.SSLDNSNames = decode.DNSNames
|
setting.SSLDNSNames = decode.DNSNames
|
||||||
}
|
}
|
||||||
|
// 伪静态
|
||||||
rewrite, _ := io.Read(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
|
rewrite, _ := io.Read(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
|
||||||
setting.Rewrite = rewrite
|
setting.Rewrite = rewrite
|
||||||
|
// 访问日志
|
||||||
log, _ := shell.Execf(`tail -n 100 '%s/wwwlogs/%s.log'`, app.Root, website.Name)
|
log, _ := shell.Execf(`tail -n 100 '%s/wwwlogs/%s.log'`, app.Root, website.Name)
|
||||||
setting.Log = log
|
setting.Log = log
|
||||||
|
|
||||||
@@ -175,21 +179,59 @@ func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
|
func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
|
||||||
w := &biz.Website{
|
// 初始化nginx配置
|
||||||
Name: req.Name,
|
p, err := nginx.NewParser()
|
||||||
Status: true,
|
if err != nil {
|
||||||
Path: req.Path,
|
return nil, err
|
||||||
PHP: req.PHP,
|
|
||||||
SSL: false,
|
|
||||||
}
|
}
|
||||||
if err := app.Orm.Create(w).Error; err != nil {
|
// 监听地址
|
||||||
|
var listens [][]string
|
||||||
|
for _, listen := range req.Listens {
|
||||||
|
listens = append(listens, []string{listen})
|
||||||
|
}
|
||||||
|
if err = p.SetListen(listens); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 域名
|
||||||
|
domains, err := punycode.EncodeDomains(req.Domains)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = p.SetServerName(domains); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 运行目录
|
||||||
|
if err = p.SetRoot(req.Path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// PHP
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// 日志
|
||||||
|
if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", req.Name+".log")); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", req.Name+".error.log")); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := io.Mkdir(req.Path, 0755); err != nil {
|
// 初始化网站目录
|
||||||
|
if err = io.Mkdir(req.Path, 0755); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html"))
|
index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("获取index模板文件失败: %w", err)
|
return nil, fmt.Errorf("获取index模板文件失败: %w", err)
|
||||||
@@ -197,92 +239,16 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
|
|||||||
if err = io.Write(filepath.Join(req.Path, "index.html"), string(index), 0644); err != nil {
|
if err = io.Write(filepath.Join(req.Path, "index.html"), string(index), 0644); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html"))
|
notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("获取404模板文件失败: %w", err)
|
return nil, fmt.Errorf("获取404模板文件失败: %w", err)
|
||||||
}
|
}
|
||||||
if err = io.Write(req.Path+"/404.html", string(notFound), 0644); err != nil {
|
if err = io.Write(filepath.Join(req.Path, "404.html"), string(notFound), 0644); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
portList := ""
|
// 写nginx配置
|
||||||
domainList := ""
|
if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), p.Dump(), 0644); err != nil {
|
||||||
portUsed := make(map[uint]bool)
|
|
||||||
domainUsed := make(map[string]bool)
|
|
||||||
|
|
||||||
for i, port := range req.Ports {
|
|
||||||
if _, ok := portUsed[port]; !ok {
|
|
||||||
if i == len(req.Ports)-1 {
|
|
||||||
portList += " listen " + cast.ToString(port) + ";\n"
|
|
||||||
portList += " listen [::]:" + cast.ToString(port) + ";"
|
|
||||||
} else {
|
|
||||||
portList += " listen " + cast.ToString(port) + ";\n"
|
|
||||||
portList += " listen [::]:" + cast.ToString(port) + ";\n"
|
|
||||||
}
|
|
||||||
portUsed[port] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, domain := range req.Domains {
|
|
||||||
if _, ok := domainUsed[domain]; !ok {
|
|
||||||
domainList += " " + domain
|
|
||||||
domainUsed[domain] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
|
||||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
|
||||||
server
|
|
||||||
{
|
|
||||||
# port标记位开始
|
|
||||||
%s
|
|
||||||
# port标记位结束
|
|
||||||
# server_name标记位开始
|
|
||||||
server_name%s;
|
|
||||||
# server_name标记位结束
|
|
||||||
# index标记位开始
|
|
||||||
index index.php index.html;
|
|
||||||
# index标记位结束
|
|
||||||
# root标记位开始
|
|
||||||
root %s;
|
|
||||||
# root标记位结束
|
|
||||||
|
|
||||||
# ssl标记位开始
|
|
||||||
# ssl标记位结束
|
|
||||||
|
|
||||||
# php标记位开始
|
|
||||||
include enable-php-%d.conf;
|
|
||||||
# php标记位结束
|
|
||||||
|
|
||||||
# 错误页配置,可自行设置
|
|
||||||
error_page 404 /404.html;
|
|
||||||
#error_page 502 /502.html;
|
|
||||||
|
|
||||||
# acme证书签发配置,不可修改
|
|
||||||
include %s/server/vhost/acme/%s.conf;
|
|
||||||
|
|
||||||
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
|
|
||||||
include %s/server/vhost/rewrite/%s.conf;
|
|
||||||
|
|
||||||
# 面板默认禁止访问部分敏感目录,可自行修改
|
|
||||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
|
|
||||||
{
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
|
|
||||||
location ~ .*\.(js|css)$
|
|
||||||
{
|
|
||||||
expires 1h;
|
|
||||||
error_log /dev/null;
|
|
||||||
access_log /dev/null;
|
|
||||||
}
|
|
||||||
|
|
||||||
access_log %s/wwwlogs/%s.log;
|
|
||||||
error_log %s/wwwlogs/%s.log;
|
|
||||||
}
|
|
||||||
`, portList, domainList, req.Path, req.PHP, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name)
|
|
||||||
|
|
||||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), nginxConf, 0644); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"), "", 0644); err != nil {
|
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"), "", 0644); err != nil {
|
||||||
@@ -291,13 +257,14 @@ server
|
|||||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"), "", 0644); err != nil {
|
if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"), "", 0644); err != nil {
|
||||||
return nil, err
|
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/cert", req.Name+".pem"), "", 0644); err != nil {
|
||||||
return nil, err
|
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+".key"), "", 0644); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置目录权限
|
||||||
if err = io.Chmod(req.Path, 0755); err != nil {
|
if err = io.Chmod(req.Path, 0755); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -305,11 +272,23 @@ server
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建面板网站
|
||||||
|
w := &biz.Website{
|
||||||
|
Name: req.Name,
|
||||||
|
Status: true,
|
||||||
|
Path: req.Path,
|
||||||
|
Https: false,
|
||||||
|
}
|
||||||
|
if err = app.Orm.Create(w).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err = systemctl.Reload("nginx"); err != nil {
|
if err = systemctl.Reload("nginx"); err != nil {
|
||||||
_, err = shell.Execf("nginx -t")
|
_, err = shell.Execf("nginx -t")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建数据库
|
||||||
rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||||
if err == nil && req.DB && req.DBType == "mysql" {
|
if err == nil && req.DB && req.DBType == "mysql" {
|
||||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
|
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
|
||||||
@@ -344,17 +323,17 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
|
|||||||
if err := app.Orm.Where("id", req.ID).First(website).Error; err != nil {
|
if err := app.Orm.Where("id", req.ID).First(website).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !website.Status {
|
if !website.Status {
|
||||||
return errors.New("网站已停用,请先启用")
|
return errors.New("网站已停用,请先启用")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原文
|
// 解析nginx配置
|
||||||
raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(raw) != strings.TrimSpace(req.Raw) {
|
// 如果修改了原文,直接写入返回
|
||||||
|
if strings.TrimSpace(config) != strings.TrimSpace(req.Raw) {
|
||||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), req.Raw, 0644); err != nil {
|
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), req.Raw, 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -362,188 +341,133 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
|
|||||||
_, err = shell.Execf("nginx -t")
|
_, err = shell.Execf("nginx -t")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 目录
|
// 初始化nginx配置
|
||||||
path := req.Path
|
p, err := nginx.NewParser(config)
|
||||||
if !io.Exists(path) {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 监听地址
|
||||||
|
var listens [][]string
|
||||||
|
for _, listen := range req.Listens {
|
||||||
|
if !listen.HTTPS && !listen.QUIC {
|
||||||
|
listens = append(listens, []string{listen.Address})
|
||||||
|
}
|
||||||
|
if listen.HTTPS {
|
||||||
|
listens = append(listens, []string{listen.Address, "ssl"})
|
||||||
|
}
|
||||||
|
if listen.QUIC {
|
||||||
|
listens = append(listens, []string{listen.Address, "quic"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = p.SetListen(listens); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 域名
|
||||||
|
domains, err := punycode.EncodeDomains(req.Domains)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.SetServerName(domains); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 首页文件
|
||||||
|
if err = p.SetIndex(req.Index); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 运行目录
|
||||||
|
if !io.Exists(req.Root) {
|
||||||
|
return errors.New("运行目录不存在")
|
||||||
|
}
|
||||||
|
if err = p.SetRoot(req.Root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 运行目录
|
||||||
|
if !io.Exists(req.Path) {
|
||||||
return errors.New("网站目录不存在")
|
return errors.New("网站目录不存在")
|
||||||
}
|
}
|
||||||
website.Path = path
|
website.Path = req.Path
|
||||||
|
// PHP
|
||||||
// 域名
|
if err = p.SetPHP(req.PHP); err != nil {
|
||||||
domain := "server_name"
|
return err
|
||||||
domains := req.Domains
|
}
|
||||||
for _, v := range domains {
|
// HTTPS
|
||||||
if v == "" {
|
certPath := filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem")
|
||||||
continue
|
keyPath := filepath.Join(app.Root, "server/vhost/cert", website.Name+".key")
|
||||||
|
if err = io.Write(certPath, req.SSLCertificate, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = io.Write(keyPath, req.SSLCertificateKey, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
website.Https = req.HTTPS
|
||||||
|
if req.HTTPS {
|
||||||
|
if err = p.SetHTTPS(certPath, keyPath); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
domain += " " + v
|
if err = p.SetHTTPRedirect(req.HTTPRedirect); err != nil {
|
||||||
}
|
return err
|
||||||
domain += ";"
|
}
|
||||||
domainConfigOld := str.Cut(raw, "# server_name标记位开始", "# server_name标记位结束")
|
if err = p.SetHSTS(req.HSTS); err != nil {
|
||||||
if len(strings.TrimSpace(domainConfigOld)) == 0 {
|
return err
|
||||||
return errors.New("配置文件中缺少server_name标记位")
|
}
|
||||||
}
|
if err = p.SetOCSP(req.OCSP); err != nil {
|
||||||
raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1)
|
return err
|
||||||
|
|
||||||
// 端口
|
|
||||||
var portConf strings.Builder
|
|
||||||
ports := req.Ports
|
|
||||||
for _, port := range ports {
|
|
||||||
https := ""
|
|
||||||
quic := false
|
|
||||||
if slices.Contains(req.SSLPorts, port) {
|
|
||||||
https = " ssl"
|
|
||||||
if slices.Contains(req.QUICPorts, port) {
|
|
||||||
quic = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, https))
|
} else {
|
||||||
portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, https))
|
if err = p.ClearSetHTTPS(); err != nil {
|
||||||
if quic {
|
return err
|
||||||
portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, " quic"))
|
}
|
||||||
portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, " quic"))
|
if err = p.SetHTTPRedirect(false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.SetHSTS(false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.SetOCSP(false); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
portConf.WriteString(" ")
|
|
||||||
portConfNew := portConf.String()
|
|
||||||
portConfOld := str.Cut(raw, "# port标记位开始", "# port标记位结束")
|
|
||||||
if len(strings.TrimSpace(portConfOld)) == 0 {
|
|
||||||
return errors.New("配置文件中缺少port标记位")
|
|
||||||
}
|
|
||||||
raw = strings.Replace(raw, portConfOld, "\n"+portConfNew, -1)
|
|
||||||
|
|
||||||
// 运行目录
|
|
||||||
root := str.Cut(raw, "# root标记位开始", "# root标记位结束")
|
|
||||||
if len(strings.TrimSpace(root)) == 0 {
|
|
||||||
return errors.New("配置文件中缺少root标记位")
|
|
||||||
}
|
|
||||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root)
|
|
||||||
if len(match) != 2 {
|
|
||||||
return errors.New("配置文件中root标记位格式错误")
|
|
||||||
}
|
|
||||||
rootNew := strings.Replace(root, match[1], req.Root, -1)
|
|
||||||
raw = strings.Replace(raw, root, rootNew, -1)
|
|
||||||
|
|
||||||
// 默认文件
|
|
||||||
index := str.Cut(raw, "# index标记位开始", "# index标记位结束")
|
|
||||||
if len(strings.TrimSpace(index)) == 0 {
|
|
||||||
return errors.New("配置文件中缺少index标记位")
|
|
||||||
}
|
|
||||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index)
|
|
||||||
if len(match) != 2 {
|
|
||||||
return errors.New("配置文件中index标记位格式错误")
|
|
||||||
}
|
|
||||||
indexNew := strings.Replace(index, match[1], req.Index, -1)
|
|
||||||
raw = strings.Replace(raw, index, indexNew, -1)
|
|
||||||
|
|
||||||
// 防跨站
|
// 防跨站
|
||||||
if !strings.HasSuffix(req.Root, "/") {
|
if !strings.HasSuffix(req.Root, "/") {
|
||||||
req.Root += "/"
|
req.Root += "/"
|
||||||
}
|
}
|
||||||
|
userIni := filepath.Join(req.Root, ".user.ini")
|
||||||
if req.OpenBasedir {
|
if req.OpenBasedir {
|
||||||
if err = io.Write(req.Root+".user.ini", "open_basedir="+path+":/tmp/", 0644); err != nil {
|
_, _ = shell.Execf(`chattr -i '%s'`, userIni)
|
||||||
|
if err = io.Write(userIni, fmt.Sprintf("open_basedir=%s:/tmp/", req.Root), 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
_, _ = shell.Execf(`chattr +i '%s'`, userIni)
|
||||||
} else {
|
} else {
|
||||||
if io.Exists(req.Root + ".user.ini") {
|
if io.Exists(userIni) {
|
||||||
if err = io.Remove(req.Root + ".user.ini"); err != nil {
|
_, _ = shell.Execf(`chattr -i '%s'`, userIni)
|
||||||
|
if err = io.Remove(userIni); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSL
|
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
|
||||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"), req.SSLCertificate, 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"), req.SSLCertificateKey, 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
website.SSL = req.SSL
|
|
||||||
if req.SSL {
|
|
||||||
if _, err = cert.ParseCert(req.SSLCertificate); err != nil {
|
|
||||||
return errors.New("TLS证书格式错误")
|
|
||||||
}
|
|
||||||
if _, err = cert.ParseKey(req.SSLCertificateKey); err != nil {
|
|
||||||
return errors.New("TLS私钥格式错误")
|
|
||||||
}
|
|
||||||
sslConfig := fmt.Sprintf(`# ssl标记位开始
|
|
||||||
ssl_certificate %s/server/vhost/ssl/%s.pem;
|
|
||||||
ssl_certificate_key %s/server/vhost/ssl/%s.key;
|
|
||||||
ssl_session_timeout 1d;
|
|
||||||
ssl_session_cache shared:SSL:10m;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers 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;
|
|
||||||
ssl_prefer_server_ciphers off;
|
|
||||||
ssl_early_data on;
|
|
||||||
`, app.Root, website.Name, app.Root, website.Name)
|
|
||||||
if req.HTTPRedirect {
|
|
||||||
sslConfig += `# http重定向标记位开始
|
|
||||||
if ($server_port !~ 443){
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
error_page 497 https://$host$request_uri;
|
|
||||||
# http重定向标记位结束
|
|
||||||
`
|
|
||||||
}
|
|
||||||
if req.HSTS {
|
|
||||||
sslConfig += `# hsts标记位开始
|
|
||||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
|
||||||
# hsts标记位结束
|
|
||||||
`
|
|
||||||
}
|
|
||||||
if req.OCSP {
|
|
||||||
sslConfig += `# ocsp标记位开始
|
|
||||||
ssl_stapling on;
|
|
||||||
ssl_stapling_verify on;
|
|
||||||
# ocsp标记位结束
|
|
||||||
`
|
|
||||||
}
|
|
||||||
sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
|
||||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
|
||||||
raw = strings.Replace(raw, sslConfigOld, "", -1)
|
|
||||||
}
|
|
||||||
raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1)
|
|
||||||
} else {
|
|
||||||
sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
|
||||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
|
||||||
raw = strings.Replace(raw, sslConfigOld, "\n ", -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if website.PHP != req.PHP {
|
|
||||||
website.PHP = req.PHP
|
|
||||||
phpConfigOld := str.Cut(raw, "# php标记位开始", "# php标记位结束")
|
|
||||||
phpConfig := `
|
|
||||||
include enable-php-` + strconv.Itoa(website.PHP) + `.conf;
|
|
||||||
`
|
|
||||||
if len(strings.TrimSpace(phpConfigOld)) != 0 {
|
|
||||||
raw = strings.Replace(raw, phpConfigOld, phpConfig, -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = app.Orm.Save(website).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), req.Rewrite, 0644); err != nil {
|
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), req.Rewrite, 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = systemctl.Reload("nginx")
|
if err = app.Orm.Save(website).Error; err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
_, err = shell.Execf("nginx -t")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
if err = systemctl.Reload("nginx"); err != nil {
|
||||||
|
_, err = shell.Execf("nginx -t")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
||||||
@@ -551,20 +475,15 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
|||||||
if err := app.Orm.Preload("Cert").Where("id", req.ID).First(website).Error; err != nil {
|
if err := app.Orm.Preload("Cert").Where("id", req.ID).First(website).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if website.Cert != nil {
|
if website.Cert != nil {
|
||||||
return errors.New("网站" + website.Name + "已绑定SSL证书,请先删除证书")
|
return errors.New("网站" + website.Name + "已绑定证书,请先删除证书")
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.Orm.Delete(website).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
_ = 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/rewrite", website.Name+".conf"))
|
||||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", 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/cert", 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+".key"))
|
||||||
|
|
||||||
if req.Path {
|
if req.Path {
|
||||||
_ = io.Remove(website.Path)
|
_ = io.Remove(website.Path)
|
||||||
@@ -584,12 +503,16 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
|||||||
_, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name)
|
_, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := systemctl.Reload("nginx")
|
if err := app.Orm.Delete(website).Error; err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
_, err = shell.Execf("nginx -t")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
if err := systemctl.Reload("nginx"); err != nil {
|
||||||
|
_, err = shell.Execf("nginx -t")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *websiteRepo) ClearLog(id uint) error {
|
func (r *websiteRepo) ClearLog(id uint) error {
|
||||||
@@ -618,75 +541,52 @@ func (r *websiteRepo) ResetConfig(id uint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
website.Status = true
|
// 初始化nginx配置
|
||||||
website.SSL = false
|
p, err := nginx.NewParser()
|
||||||
if err := app.Orm.Save(website).Error; err != nil {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 运行目录
|
||||||
|
if err = p.SetRoot(website.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 伪静态
|
||||||
|
includes, comments, err := p.GetIncludes()
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// 日志
|
||||||
|
if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", website.Name+".log")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", website.Name+".error.log")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
raw := fmt.Sprintf(`
|
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
|
||||||
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
|
||||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
|
||||||
server
|
|
||||||
{
|
|
||||||
# port标记位开始
|
|
||||||
listen 80;
|
|
||||||
# port标记位结束
|
|
||||||
# server_name标记位开始
|
|
||||||
server_name localhost;
|
|
||||||
# server_name标记位结束
|
|
||||||
# index标记位开始
|
|
||||||
index index.php index.html;
|
|
||||||
# index标记位结束
|
|
||||||
# root标记位开始
|
|
||||||
root %s;
|
|
||||||
# root标记位结束
|
|
||||||
|
|
||||||
# ssl标记位开始
|
|
||||||
# ssl标记位结束
|
|
||||||
|
|
||||||
# php标记位开始
|
|
||||||
include enable-php-%d.conf;
|
|
||||||
# php标记位结束
|
|
||||||
|
|
||||||
# 错误页配置,可自行设置
|
|
||||||
error_page 404 /404.html;
|
|
||||||
#error_page 502 /502.html;
|
|
||||||
|
|
||||||
# acme证书签发配置,不可修改
|
|
||||||
include %s/server/vhost/acme/%s.conf;
|
|
||||||
|
|
||||||
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
|
|
||||||
include %s/server/vhost/rewrite/%s.conf;
|
|
||||||
|
|
||||||
# 面板默认禁止访问部分敏感目录,可自行修改
|
|
||||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
|
|
||||||
{
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
|
|
||||||
location ~ .*\.(js|css)$
|
|
||||||
{
|
|
||||||
expires 1h;
|
|
||||||
error_log /dev/null;
|
|
||||||
access_log /dev/null;
|
|
||||||
}
|
|
||||||
|
|
||||||
access_log %s/wwwlogs/%s.log;
|
|
||||||
error_log %s/wwwlogs/%s.log;
|
|
||||||
}
|
|
||||||
|
|
||||||
`, website.Path, website.PHP, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name)
|
|
||||||
if err := io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil {
|
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil {
|
if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
if err := systemctl.Reload("nginx"); err != nil {
|
|
||||||
|
website.Status = true
|
||||||
|
website.Https = false
|
||||||
|
if err = app.Orm.Save(website).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = systemctl.Reload("nginx"); err != nil {
|
||||||
_, err = shell.Execf("nginx -t")
|
_, err = shell.Execf("nginx -t")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -700,43 +600,69 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
website.Status = status
|
// 解析nginx配置
|
||||||
if err := app.Orm.Save(website).Error; err != nil {
|
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
p, err := nginx.NewParser(config)
|
||||||
raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行目录
|
// 取运行目录和默认文档
|
||||||
rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束")
|
root, rootComment, err := p.GetRootWithComment()
|
||||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
|
if err != nil {
|
||||||
if len(match) == 2 {
|
|
||||||
if website.Status {
|
|
||||||
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
|
|
||||||
raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s;\n ", root[1]))
|
|
||||||
} else {
|
|
||||||
raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s/server/nginx/html;\n # root %s;\n ", app.Root, match[1]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认文件
|
|
||||||
indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束")
|
|
||||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
|
|
||||||
if len(match) == 2 {
|
|
||||||
if website.Status {
|
|
||||||
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
|
|
||||||
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
|
|
||||||
} else {
|
|
||||||
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
index, indexComment, err := p.GetIndexWithComment()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
indexStr := strings.Join(index, " ")
|
||||||
|
|
||||||
|
if status {
|
||||||
|
if len(rootComment) == 0 {
|
||||||
|
return fmt.Errorf("未找到运行目录注释")
|
||||||
|
}
|
||||||
|
if len(rootComment) != 1 {
|
||||||
|
return fmt.Errorf("运行目录注释数量不正确,预期1个,实际%d个", len(rootComment))
|
||||||
|
}
|
||||||
|
rootComment[0] = strings.TrimPrefix(rootComment[0], "# ")
|
||||||
|
if !io.Exists(rootComment[0]) {
|
||||||
|
return fmt.Errorf("运行目录不存在")
|
||||||
|
}
|
||||||
|
if err = p.SetRoot(rootComment[0]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(indexComment) == 0 {
|
||||||
|
return fmt.Errorf("未找到默认文档注释")
|
||||||
|
}
|
||||||
|
if len(indexComment) != 1 {
|
||||||
|
return fmt.Errorf("默认文档注释数量不正确,预期1个,实际%d个", len(indexComment))
|
||||||
|
}
|
||||||
|
indexComment[0] = strings.TrimPrefix(indexComment[0], "# ")
|
||||||
|
if err = p.SetIndex(strings.Fields(indexComment[0])); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err = p.SetRootWithComment(filepath.Join(app.Root, "server/nginx/html"), []string{"# " + root}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.SetIndexWithComment([]string{"stop.html"}, []string{"# " + indexStr}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
website.Status = status
|
||||||
|
if err = app.Orm.Save(website).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err = systemctl.Reload("nginx"); err != nil {
|
if err = systemctl.Reload("nginx"); err != nil {
|
||||||
_, err = shell.Execf("nginx -t")
|
_, err = shell.Execf("nginx -t")
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,35 +5,35 @@ import (
|
|||||||
|
|
||||||
"github.com/go-rat/chix"
|
"github.com/go-rat/chix"
|
||||||
|
|
||||||
"github.com/TheTNB/panel/pkg/types"
|
"github.com/TheTNB/panel/internal/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status 检查程序状态
|
// Status 检查程序状态
|
||||||
func Status(next http.Handler) http.Handler {
|
func Status(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch types.Status {
|
switch app.Status {
|
||||||
case types.StatusUpgrade:
|
case app.StatusUpgrade:
|
||||||
render := chix.NewRender(w)
|
render := chix.NewRender(w)
|
||||||
render.Status(http.StatusServiceUnavailable)
|
render.Status(http.StatusServiceUnavailable)
|
||||||
render.JSON(chix.M{
|
render.JSON(chix.M{
|
||||||
"message": "面板升级中,请稍后刷新",
|
"message": "面板升级中,请稍后刷新",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
case types.StatusMaintain:
|
case app.StatusMaintain:
|
||||||
render := chix.NewRender(w)
|
render := chix.NewRender(w)
|
||||||
render.Status(http.StatusServiceUnavailable)
|
render.Status(http.StatusServiceUnavailable)
|
||||||
render.JSON(chix.M{
|
render.JSON(chix.M{
|
||||||
"message": "面板正在运行维护任务,请稍后刷新",
|
"message": "面板正在运行维护任务,请稍后刷新",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
case types.StatusClosed:
|
case app.StatusClosed:
|
||||||
render := chix.NewRender(w)
|
render := chix.NewRender(w)
|
||||||
render.Status(http.StatusForbidden)
|
render.Status(http.StatusForbidden)
|
||||||
render.JSON(chix.M{
|
render.JSON(chix.M{
|
||||||
"message": "面板已关闭",
|
"message": "面板已关闭",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
case types.StatusFailed:
|
case app.StatusFailed:
|
||||||
render := chix.NewRender(w)
|
render := chix.NewRender(w)
|
||||||
render.Status(http.StatusInternalServerError)
|
render.Status(http.StatusInternalServerError)
|
||||||
render.JSON(chix.M{
|
render.JSON(chix.M{
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Slug string `json:"slug" form:"slug"`
|
Slug string `json:"slug" form:"slug" validate:"required,not_exists=apps slug"`
|
||||||
Channel string `json:"channel" form:"channel"`
|
Channel string `json:"channel" form:"channel" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppSlug struct {
|
type AppSlug struct {
|
||||||
Slug string `json:"slug" form:"slug"`
|
Slug string `json:"slug" form:"slug" validate:"required,exists=apps slug"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppUpdateShow struct {
|
type AppUpdateShow struct {
|
||||||
Slug string `json:"slug" form:"slug"`
|
Slug string `json:"slug" form:"slug" validate:"required,exists=apps slug"`
|
||||||
Show bool `json:"show" form:"show"`
|
Show bool `json:"show" form:"show"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type CertCreate struct {
|
type CertCreate struct {
|
||||||
Type string `form:"type" json:"type"`
|
Type string `form:"type" json:"type" validate:"required"`
|
||||||
Domains []string `form:"domains" json:"domains"`
|
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||||
AccountID uint `form:"account_id" json:"account_id"`
|
AccountID uint `form:"account_id" json:"account_id"`
|
||||||
DNSID uint `form:"dns_id" json:"dns_id"`
|
DNSID uint `form:"dns_id" json:"dns_id"`
|
||||||
@@ -10,9 +10,9 @@ type CertCreate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CertUpdate struct {
|
type CertUpdate struct {
|
||||||
ID uint `form:"id" json:"id"`
|
ID uint `form:"id" json:"id" validate:"required,exists=certs id"`
|
||||||
Type string `form:"type" json:"type"`
|
Type string `form:"type" json:"type" validate:"required"`
|
||||||
Domains []string `form:"domains" json:"domains"`
|
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||||
AccountID uint `form:"account_id" json:"account_id"`
|
AccountID uint `form:"account_id" json:"account_id"`
|
||||||
DNSID uint `form:"dns_id" json:"dns_id"`
|
DNSID uint `form:"dns_id" json:"dns_id"`
|
||||||
@@ -20,6 +20,6 @@ type CertUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CertDeploy struct {
|
type CertDeploy struct {
|
||||||
ID uint `form:"id" json:"id"`
|
ID uint `form:"id" json:"id" validate:"required,exists=certs id"`
|
||||||
WebsiteID uint `form:"website_id" json:"website_id"`
|
WebsiteID uint `form:"website_id" json:"website_id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type CertAccountCreate struct {
|
type CertAccountCreate struct {
|
||||||
CA string `form:"ca" json:"ca"`
|
CA string `form:"ca" json:"ca" validate:"required"`
|
||||||
Email string `form:"email" json:"email"`
|
Email string `form:"email" json:"email" validate:"required"`
|
||||||
Kid string `form:"kid" json:"kid"`
|
Kid string `form:"kid" json:"kid"`
|
||||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||||
KeyType string `form:"key_type" json:"key_type"`
|
KeyType string `form:"key_type" json:"key_type" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CertAccountUpdate struct {
|
type CertAccountUpdate struct {
|
||||||
ID uint `form:"id" json:"id"`
|
ID uint `form:"id" json:"id" validate:"required,exists=cert_accounts id"`
|
||||||
CA string `form:"ca" json:"ca"`
|
CA string `form:"ca" json:"ca" validate:"required"`
|
||||||
Email string `form:"email" json:"email"`
|
Email string `form:"email" json:"email" validate:"required"`
|
||||||
Kid string `form:"kid" json:"kid"`
|
Kid string `form:"kid" json:"kid"`
|
||||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||||
KeyType string `form:"key_type" json:"key_type"`
|
KeyType string `form:"key_type" json:"key_type" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package request
|
|||||||
import "github.com/TheTNB/panel/pkg/acme"
|
import "github.com/TheTNB/panel/pkg/acme"
|
||||||
|
|
||||||
type CertDNSCreate struct {
|
type CertDNSCreate struct {
|
||||||
Type string `form:"type" json:"type"`
|
Type string `form:"type" json:"type" validate:"required"`
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Data acme.DNSParam `form:"data" json:"data"`
|
Data acme.DNSParam `form:"data" json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CertDNSUpdate struct {
|
type CertDNSUpdate struct {
|
||||||
ID uint `form:"id" json:"id"`
|
ID uint `form:"id" json:"id" validate:"required,exists=cert_dns id"`
|
||||||
Type string `form:"type" json:"type"`
|
Type string `form:"type" json:"type" validate:"required"`
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Data acme.DNSParam `form:"data" json:"data"`
|
Data acme.DNSParam `form:"data" json:"data"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ package request
|
|||||||
import "github.com/TheTNB/panel/pkg/types"
|
import "github.com/TheTNB/panel/pkg/types"
|
||||||
|
|
||||||
type ContainerID struct {
|
type ContainerID struct {
|
||||||
ID string `json:"id" form:"id"`
|
ID string `json:"id" form:"id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerRename struct {
|
type ContainerRename struct {
|
||||||
ID string `form:"id" json:"id"`
|
ID string `form:"id" json:"id" validate:"required"`
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerCreate struct {
|
type ContainerCreate struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Image string `form:"image" json:"image"`
|
Image string `form:"image" json:"image" validate:"required"`
|
||||||
Ports []types.ContainerPort `form:"ports" json:"ports"`
|
Ports []types.ContainerPort `form:"ports" json:"ports"`
|
||||||
Network string `form:"network" json:"network"`
|
Network string `form:"network" json:"network"`
|
||||||
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
|
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ type ContainerImageID struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ContainerImagePull struct {
|
type ContainerImagePull struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Auth bool `form:"auth" json:"auth"`
|
Auth bool `form:"auth" json:"auth"`
|
||||||
Username string `form:"username" json:"username"`
|
Username string `form:"username" json:"username"`
|
||||||
Password string `form:"password" json:"password"`
|
Password string `form:"password" json:"password"`
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package request
|
|||||||
import "github.com/TheTNB/panel/pkg/types"
|
import "github.com/TheTNB/panel/pkg/types"
|
||||||
|
|
||||||
type ContainerNetworkID struct {
|
type ContainerNetworkID struct {
|
||||||
ID string `json:"id" form:"id"`
|
ID string `json:"id" form:"id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerNetworkCreate struct {
|
type ContainerNetworkCreate struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Driver string `form:"driver" json:"driver"`
|
Driver string `form:"driver" json:"driver"`
|
||||||
Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"`
|
Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"`
|
||||||
Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"`
|
Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"`
|
||||||
@@ -16,6 +16,6 @@ type ContainerNetworkCreate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ContainerNetworkConnect struct {
|
type ContainerNetworkConnect struct {
|
||||||
Network string `form:"network" json:"network"`
|
Network string `form:"network" json:"network" validate:"required"`
|
||||||
Container string `form:"container" json:"container"`
|
Container string `form:"container" json:"container" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package request
|
|||||||
import "github.com/TheTNB/panel/pkg/types"
|
import "github.com/TheTNB/panel/pkg/types"
|
||||||
|
|
||||||
type ContainerVolumeID struct {
|
type ContainerVolumeID struct {
|
||||||
ID string `json:"id" form:"id"`
|
ID string `json:"id" form:"id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerVolumeCreate struct {
|
type ContainerVolumeCreate struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Driver string `form:"driver" json:"driver"`
|
Driver string `form:"driver" json:"driver"`
|
||||||
Labels []types.KV `form:"labels" json:"labels"`
|
Labels []types.KV `form:"labels" json:"labels"`
|
||||||
Options []types.KV `form:"options" json:"options"`
|
Options []types.KV `form:"options" json:"options"`
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type CronCreate struct {
|
type CronCreate struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required,not_exists=crons name"`
|
||||||
Type string `form:"type" json:"type"`
|
Type string `form:"type" json:"type" validate:"required"`
|
||||||
Time string `form:"time" json:"time"`
|
Time string `form:"time" json:"time" validate:"required,cron"`
|
||||||
Script string `form:"script" json:"script"`
|
Script string `form:"script" json:"script"`
|
||||||
BackupType string `form:"backup_type" json:"backup_type"`
|
BackupType string `form:"backup_type" json:"backup_type" validate:"required"`
|
||||||
BackupPath string `form:"backup_path" json:"backup_path"`
|
BackupPath string `form:"backup_path" json:"backup_path"`
|
||||||
Target string `form:"target" json:"target"`
|
Target string `form:"target" json:"target" validate:"required"`
|
||||||
Save int `form:"save" json:"save"`
|
Save int `form:"save" json:"save" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronUpdate struct {
|
type CronUpdate struct {
|
||||||
ID uint `form:"id" json:"id"`
|
ID uint `form:"id" json:"id" validate:"required,exists=crons id"`
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required"`
|
||||||
Time string `form:"time" json:"time"`
|
Time string `form:"time" json:"time" validate:"required,cron"`
|
||||||
Script string `form:"script" json:"script"`
|
Script string `form:"script" json:"script" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronStatus struct {
|
type CronStatus struct {
|
||||||
ID uint `form:"id" json:"id"`
|
ID uint `form:"id" json:"id" validate:"required,exists=crons id"`
|
||||||
Status bool `form:"status" json:"status"`
|
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"`
|
||||||
|
}
|
||||||
@@ -1,49 +1,49 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type FilePath struct {
|
type FilePath struct {
|
||||||
Path string `json:"path" form:"path"`
|
Path string `json:"path" form:"path" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileCreate struct {
|
type FileCreate struct {
|
||||||
Dir bool `json:"dir" form:"dir"`
|
Dir bool `json:"dir" form:"dir"`
|
||||||
Path string `json:"path" form:"path"`
|
Path string `json:"path" form:"path" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileSave struct {
|
type FileSave struct {
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path" validate:"required"`
|
||||||
Content string `form:"content" json:"content"`
|
Content string `form:"content" json:"content" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileMove struct {
|
type FileMove struct {
|
||||||
Source string `form:"source" json:"source"`
|
Source string `form:"source" json:"source" validate:"required"`
|
||||||
Target string `form:"target" json:"target"`
|
Target string `form:"target" json:"target" validate:"required"`
|
||||||
Force bool `form:"force" json:"force"`
|
Force bool `form:"force" json:"force"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileCopy struct {
|
type FileCopy struct {
|
||||||
Source string `form:"source" json:"source"`
|
Source string `form:"source" json:"source" validate:"required"`
|
||||||
Target string `form:"target" json:"target"`
|
Target string `form:"target" json:"target" validate:"required"`
|
||||||
Force bool `form:"force" json:"force"`
|
Force bool `form:"force" json:"force"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilePermission struct {
|
type FilePermission struct {
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path" validate:"required"`
|
||||||
Mode string `form:"mode" json:"mode"`
|
Mode string `form:"mode" json:"mode" validate:"required"`
|
||||||
Owner string `form:"owner" json:"owner"`
|
Owner string `form:"owner" json:"owner" validate:"required"`
|
||||||
Group string `form:"group" json:"group"`
|
Group string `form:"group" json:"group" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileCompress struct {
|
type FileCompress struct {
|
||||||
Paths []string `form:"paths" json:"paths"`
|
Paths []string `form:"paths" json:"paths" validate:"min=1,dive,required"`
|
||||||
File string `form:"file" json:"file"`
|
File string `form:"file" json:"file" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileUnCompress struct {
|
type FileUnCompress struct {
|
||||||
File string `form:"file" json:"file"`
|
File string `form:"file" json:"file" validate:"required"`
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileSearch struct {
|
type FileSearch struct {
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path" validate:"required"`
|
||||||
KeyWord string `form:"keyword" json:"keyword"`
|
KeyWord string `form:"keyword" json:"keyword" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ type FirewallStatus struct {
|
|||||||
Status bool `json:"status" form:"status"`
|
Status bool `json:"status" form:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallCreateRule struct {
|
type FirewallRule struct {
|
||||||
Port uint `json:"port"`
|
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||||
Protocol string `json:"protocol"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type SafeUpdateSSH struct {
|
type SafeUpdateSSH struct {
|
||||||
Port uint `json:"port" form:"port"`
|
Port uint `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
|
||||||
Status bool `json:"status" form:"status"`
|
Status bool `json:"status" form:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type PanelSetting struct {
|
type PanelSetting struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name" validate:"required"`
|
||||||
Locale string `json:"locale"`
|
Locale string `json:"locale" validate:"required"`
|
||||||
Entrance string `json:"entrance"`
|
Entrance string `json:"entrance" validate:"required"`
|
||||||
WebsitePath string `json:"website_path"`
|
OfflineMode bool `json:"offline_mode"`
|
||||||
BackupPath string `json:"backup_path"`
|
WebsitePath string `json:"website_path" validate:"required"`
|
||||||
Username string `json:"username"`
|
BackupPath string `json:"backup_path" validate:"required"`
|
||||||
|
Username string `json:"username" validate:"required"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email" validate:"required"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port" validate:"required,number,gte=1,lte=65535"`
|
||||||
HTTPS bool `json:"https"`
|
HTTPS bool `json:"https"`
|
||||||
Cert string `json:"cert"`
|
Cert string `json:"cert" validate:"required"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type SSHUpdateInfo struct {
|
type SSHUpdateInfo struct {
|
||||||
Host string `json:"host" form:"host"`
|
Host string `json:"host" form:"host" validate:"required"`
|
||||||
Port int `json:"port" form:"port"`
|
Port int `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
|
||||||
User string `json:"user" form:"user"`
|
User string `json:"user" form:"user" validate:"required"`
|
||||||
Password string `json:"password" form:"password"`
|
Password string `json:"password" form:"password" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type SystemctlService struct {
|
type SystemctlService struct {
|
||||||
Service string `json:"service"`
|
Service string `json:"service" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,3 @@ type UserLogin struct {
|
|||||||
Username string `json:"username" form:"username" validate:"required,min=3,max=255"`
|
Username string `json:"username" form:"username" validate:"required,min=3,max=255"`
|
||||||
Password string `json:"password" form:"password" validate:"required,min=6,max=255"`
|
Password string `json:"password" form:"password" validate:"required,min=6,max=255"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserID struct {
|
|
||||||
ID uint `uri:"id" validate:"required,number"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddUser struct {
|
|
||||||
Name string `json:"name" form:"name" validate:"required,min=3,max=255" comment:"用户名"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
|
import "github.com/TheTNB/panel/pkg/types"
|
||||||
|
|
||||||
type WebsiteDefaultConfig struct {
|
type WebsiteDefaultConfig struct {
|
||||||
Index string `json:"index" form:"index"`
|
Index string `json:"index" form:"index" validate:"required"`
|
||||||
Stop string `json:"stop" form:"stop"`
|
Stop string `json:"stop" form:"stop" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteCreate struct {
|
type WebsiteCreate struct {
|
||||||
Name string `form:"name" json:"name"`
|
Name string `form:"name" json:"name" validate:"required,not_exists=websites name"`
|
||||||
Domains []string `form:"domains" json:"domains"`
|
Listens []string `form:"listens" json:"listens" validate:"min=1,dive,required"`
|
||||||
Ports []uint `form:"ports" json:"ports"`
|
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||||
Path string `form:"path" json:"path"`
|
Path string `form:"path" json:"path"`
|
||||||
PHP int `form:"php" json:"php"`
|
PHP int `form:"php" json:"php" validate:"number,gte=0"`
|
||||||
DB bool `form:"db" json:"db"`
|
DB bool `form:"db" json:"db"`
|
||||||
DBType string `form:"db_type" json:"db_type"`
|
DBType string `form:"db_type" json:"db_type"`
|
||||||
DBName string `form:"db_name" json:"db_name"`
|
DBName string `form:"db_name" json:"db_name"`
|
||||||
@@ -19,38 +21,36 @@ type WebsiteCreate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteDelete struct {
|
type WebsiteDelete struct {
|
||||||
ID uint `form:"id" json:"id"`
|
ID uint `form:"id" json:"id" validate:"required,exists=websites id"`
|
||||||
Path bool `form:"path" json:"path"`
|
Path bool `form:"path" json:"path"`
|
||||||
DB bool `form:"db" json:"db"`
|
DB bool `form:"db" json:"db"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteUpdate struct {
|
type WebsiteUpdate struct {
|
||||||
ID uint `form:"id" json:"id"`
|
ID uint `form:"id" json:"id" validate:"required,exists=websites id"`
|
||||||
Domains []string `form:"domains" json:"domains"`
|
Listens []types.WebsiteListen `form:"listens" json:"listens" validate:"min=1"`
|
||||||
Ports []uint `form:"ports" json:"ports"`
|
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||||
SSLPorts []uint `form:"ssl_ports" json:"ssl_ports"`
|
HTTPS bool `form:"https" json:"https"`
|
||||||
QUICPorts []uint `form:"quic_ports" json:"quic_ports"`
|
OCSP bool `form:"ocsp" json:"ocsp"`
|
||||||
OCSP bool `form:"ocsp" json:"ocsp"`
|
HSTS bool `form:"hsts" json:"hsts"`
|
||||||
HSTS bool `form:"hsts" json:"hsts"`
|
HTTPRedirect bool `form:"http_redirect" json:"http_redirect"`
|
||||||
SSL bool `form:"ssl" json:"ssl"`
|
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
|
||||||
HTTPRedirect bool `form:"http_redirect" json:"http_redirect"`
|
Index []string `form:"index" json:"index" validate:"min=1,dive,required"`
|
||||||
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
|
Path string `form:"path" json:"path" validate:"required"` // 网站目录
|
||||||
Index string `form:"index" json:"index"`
|
Root string `form:"root" json:"root" validate:"required"` // 运行目录
|
||||||
Path string `form:"path" json:"path"`
|
Raw string `form:"raw" json:"raw"`
|
||||||
Root string `form:"root" json:"root"`
|
Rewrite string `form:"rewrite" json:"rewrite"`
|
||||||
Raw string `form:"raw" json:"raw"`
|
PHP int `form:"php" json:"php"`
|
||||||
Rewrite string `form:"rewrite" json:"rewrite"`
|
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"required_if=HTTPS true"`
|
||||||
PHP int `form:"php" json:"php"`
|
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"required_if=HTTPS true"`
|
||||||
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"`
|
|
||||||
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteUpdateRemark struct {
|
type WebsiteUpdateRemark struct {
|
||||||
ID uint `form:"id" json:"id"`
|
ID uint `form:"id" json:"id" validate:"required,exists=websites id"`
|
||||||
Remark string `form:"remark" json:"remark"`
|
Remark string `form:"remark" json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteUpdateStatus struct {
|
type WebsiteUpdateStatus struct {
|
||||||
ID uint `json:"id" form:"id"`
|
ID uint `json:"id" form:"id" validate:"required,exists=websites id"`
|
||||||
Status bool `json:"status" form:"status"`
|
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,26 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Regexp struct{}
|
||||||
|
|
||||||
|
func NewRegexp() *Regexp {
|
||||||
|
return &Regexp{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Regexp) Regexp(fl validator.FieldLevel) bool {
|
||||||
|
// 从标签中获取正则,格式类似于 `regexp=^[a-zA-Z0-9_]+$`
|
||||||
|
pattern := fl.Param()
|
||||||
|
value := fl.Field().String()
|
||||||
|
|
||||||
|
re, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return re.MatchString(value)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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("regexp", NewRegexp().Regexp); 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("regexp", *app.Translator,
|
||||||
|
func(ut ut.Translator) error {
|
||||||
|
return ut.Add("regexp", "{0} 格式不正确", true)
|
||||||
|
},
|
||||||
|
func(ut ut.Translator, fe validator.FieldError) string {
|
||||||
|
t, _ := ut.T("regexp", fe.Field())
|
||||||
|
return t
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/TheTNB/panel/internal/biz"
|
"github.com/TheTNB/panel/internal/biz"
|
||||||
"github.com/TheTNB/panel/internal/data"
|
"github.com/TheTNB/panel/internal/data"
|
||||||
pkgcert "github.com/TheTNB/panel/pkg/cert"
|
pkgcert "github.com/TheTNB/panel/pkg/cert"
|
||||||
"github.com/TheTNB/panel/pkg/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CertRenew 证书续签
|
// CertRenew 证书续签
|
||||||
@@ -23,8 +22,8 @@ func NewCertRenew() *CertRenew {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (receiver *CertRenew) Run() {
|
func (r *CertRenew) Run() {
|
||||||
if types.Status != types.StatusNormal {
|
if app.Status != app.StatusNormal {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +49,7 @@ func (receiver *CertRenew) Run() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = receiver.certRepo.Renew(cert.ID)
|
_, err = r.certRepo.Renew(cert.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger.Error("续签证书失败", zap.Error(err))
|
app.Logger.Error("续签证书失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/TheTNB/panel/internal/biz"
|
"github.com/TheTNB/panel/internal/biz"
|
||||||
"github.com/TheTNB/panel/internal/data"
|
"github.com/TheTNB/panel/internal/data"
|
||||||
"github.com/TheTNB/panel/pkg/tools"
|
"github.com/TheTNB/panel/pkg/tools"
|
||||||
"github.com/TheTNB/panel/pkg/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Monitoring 系统监控
|
// Monitoring 系统监控
|
||||||
@@ -24,8 +23,8 @@ func NewMonitoring() *Monitoring {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (receiver *Monitoring) Run() {
|
func (r *Monitoring) Run() {
|
||||||
if types.Status != types.StatusNormal {
|
if app.Status != app.StatusNormal {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,18 +32,18 @@ func (receiver *Monitoring) Run() {
|
|||||||
//task := data.NewTaskRepo()
|
//task := data.NewTaskRepo()
|
||||||
//_ = task.DispatchWaiting()
|
//_ = task.DispatchWaiting()
|
||||||
|
|
||||||
monitor, err := receiver.settingRepo.Get(biz.SettingKeyMonitor)
|
monitor, err := r.settingRepo.Get(biz.SettingKeyMonitor)
|
||||||
if err != nil || !cast.ToBool(monitor) {
|
if err != nil || !cast.ToBool(monitor) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info := tools.GetMonitoringInfo()
|
info := tools.CurrentInfo(nil, nil)
|
||||||
|
|
||||||
// 去除部分数据以减少数据库存储
|
// 去除部分数据以减少数据库存储
|
||||||
info.Disk = nil
|
info.Disk = nil
|
||||||
info.Cpus = nil
|
info.Cpus = nil
|
||||||
|
|
||||||
if types.Status != types.StatusNormal {
|
if app.Status != app.StatusNormal {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
day := cast.ToInt(dayStr)
|
day := cast.ToInt(dayStr)
|
||||||
if day <= 0 || types.Status != types.StatusNormal {
|
if day <= 0 || app.Status != app.StatusNormal {
|
||||||
return
|
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 {
|
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,52 +9,59 @@ import (
|
|||||||
"github.com/TheTNB/panel/internal/app"
|
"github.com/TheTNB/panel/internal/app"
|
||||||
"github.com/TheTNB/panel/internal/biz"
|
"github.com/TheTNB/panel/internal/biz"
|
||||||
"github.com/TheTNB/panel/internal/data"
|
"github.com/TheTNB/panel/internal/data"
|
||||||
"github.com/TheTNB/panel/pkg/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PanelTask 面板每日任务
|
// PanelTask 面板每日任务
|
||||||
type PanelTask struct {
|
type PanelTask struct {
|
||||||
appRepo biz.AppRepo
|
appRepo biz.AppRepo
|
||||||
backupRepo biz.BackupRepo
|
backupRepo biz.BackupRepo
|
||||||
|
settingRepo biz.SettingRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPanelTask() *PanelTask {
|
func NewPanelTask() *PanelTask {
|
||||||
return &PanelTask{
|
return &PanelTask{
|
||||||
appRepo: data.NewAppRepo(),
|
appRepo: data.NewAppRepo(),
|
||||||
backupRepo: data.NewBackupRepo(),
|
backupRepo: data.NewBackupRepo(),
|
||||||
|
settingRepo: data.NewSettingRepo(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (receiver *PanelTask) Run() {
|
func (r *PanelTask) Run() {
|
||||||
types.Status = types.StatusMaintain
|
app.Status = app.StatusMaintain
|
||||||
|
|
||||||
// 优化数据库
|
// 优化数据库
|
||||||
if err := app.Orm.Exec("VACUUM").Error; err != nil {
|
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 {
|
||||||
|
app.Status = app.StatusFailed
|
||||||
app.Logger.Error("优化面板数据库失败", zap.Error(err))
|
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))
|
app.Logger.Error("备份面板失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理备份
|
// 清理备份
|
||||||
path, err := receiver.backupRepo.GetPath("panel")
|
path, err := r.backupRepo.GetPath("panel")
|
||||||
if err == nil {
|
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))
|
app.Logger.Error("清理面板备份失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新商店缓存
|
// 更新商店缓存
|
||||||
if err = receiver.appRepo.UpdateCache(); err != nil {
|
if offline, err := r.settingRepo.GetBool(biz.SettingKeyOfflineMode); err == nil && !offline {
|
||||||
app.Logger.Error("更新商店缓存失败", zap.Error(err))
|
if err = r.appRepo.UpdateCache(); err != nil {
|
||||||
|
app.Logger.Error("更新商店缓存失败", zap.Error(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回收内存
|
// 回收内存
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
debug.FreeOSMemory()
|
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)
|
taskID, ok := args[0].(uint)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("参数错误")
|
return errors.New("参数错误")
|
||||||
}
|
}
|
||||||
receiver.taskID = taskID
|
r.taskID = taskID
|
||||||
|
|
||||||
task, err := receiver.taskRepo.Get(taskID)
|
task, err := r.taskRepo.Get(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = receiver.taskRepo.UpdateStatus(taskID, biz.TaskStatusRunning); err != nil {
|
if err = r.taskRepo.UpdateStatus(taskID, biz.TaskStatusRunning); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,13 +40,13 @@ func (receiver *ProcessTask) Handle(args ...any) error {
|
|||||||
return err
|
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 err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (receiver *ProcessTask) ErrHandle(err error) {
|
func (r *ProcessTask) ErrHandle(err error) {
|
||||||
_ = receiver.taskRepo.UpdateStatus(receiver.taskID, biz.TaskStatusFailed)
|
_ = r.taskRepo.UpdateStatus(r.taskID, biz.TaskStatusFailed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ func Cli() []*cli.Command {
|
|||||||
Usage: "升级面板",
|
Usage: "升级面板",
|
||||||
Action: cliService.Update,
|
Action: cliService.Update,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "fix",
|
||||||
|
Usage: "修复面板",
|
||||||
|
Action: cliService.Fix,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "info",
|
Name: "info",
|
||||||
Usage: "输出面板基本信息",
|
Usage: "输出面板基本信息",
|
||||||
@@ -113,10 +118,10 @@ func Cli() []*cli.Command {
|
|||||||
Aliases: []string{"d"},
|
Aliases: []string{"d"},
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.UintSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "ports",
|
Name: "listens",
|
||||||
Usage: "网站使用的端口列表",
|
Usage: "与网站关联的监听地址列表",
|
||||||
Aliases: []string{"p"},
|
Aliases: []string{"l"},
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
|
|||||||
@@ -23,18 +23,18 @@ func Http(r chi.Router) {
|
|||||||
r.With(middleware.MustLogin).Get("/info", user.Info)
|
r.With(middleware.MustLogin).Get("/info", user.Info)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/info", func(r chi.Router) {
|
r.Route("/dashboard", func(r chi.Router) {
|
||||||
info := service.NewInfoService()
|
dashboard := service.NewDashboardService()
|
||||||
r.Get("/panel", info.Panel)
|
r.Get("/panel", dashboard.Panel)
|
||||||
r.With(middleware.MustLogin).Get("/homeApps", info.HomeApps)
|
r.With(middleware.MustLogin).Get("/homeApps", dashboard.HomeApps)
|
||||||
r.With(middleware.MustLogin).Get("/realtime", info.Realtime)
|
r.With(middleware.MustLogin).Post("/current", dashboard.Current)
|
||||||
r.With(middleware.MustLogin).Get("/systemInfo", info.SystemInfo)
|
r.With(middleware.MustLogin).Get("/systemInfo", dashboard.SystemInfo)
|
||||||
r.With(middleware.MustLogin).Get("/countInfo", info.CountInfo)
|
r.With(middleware.MustLogin).Get("/countInfo", dashboard.CountInfo)
|
||||||
r.With(middleware.MustLogin).Get("/installedDbAndPhp", info.InstalledDbAndPhp)
|
r.With(middleware.MustLogin).Get("/installedDbAndPhp", dashboard.InstalledDbAndPhp)
|
||||||
r.With(middleware.MustLogin).Get("/checkUpdate", info.CheckUpdate)
|
r.With(middleware.MustLogin).Get("/checkUpdate", dashboard.CheckUpdate)
|
||||||
r.With(middleware.MustLogin).Get("/updateInfo", info.UpdateInfo)
|
r.With(middleware.MustLogin).Get("/updateInfo", dashboard.UpdateInfo)
|
||||||
r.With(middleware.MustLogin).Post("/update", info.Update)
|
r.With(middleware.MustLogin).Post("/update", dashboard.Update)
|
||||||
r.With(middleware.MustLogin).Post("/restart", info.Restart)
|
r.With(middleware.MustLogin).Post("/restart", dashboard.Restart)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/task", func(r chi.Router) {
|
r.Route("/task", func(r chi.Router) {
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AppService struct {
|
type AppService struct {
|
||||||
appRepo biz.AppRepo
|
appRepo biz.AppRepo
|
||||||
|
settingRepo biz.SettingRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAppService() *AppService {
|
func NewAppService() *AppService {
|
||||||
return &AppService{
|
return &AppService{
|
||||||
appRepo: data.NewAppRepo(),
|
appRepo: data.NewAppRepo(),
|
||||||
|
settingRepo: data.NewSettingRepo(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +164,11 @@ func (s *AppService) IsInstalled(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *AppService) UpdateCache(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 {
|
if err := s.appRepo.UpdateCache(); err != nil {
|
||||||
Error(w, http.StatusInternalServerError, "%v", err)
|
Error(w, http.StatusInternalServerError, "%v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func Bind[T any](r *http.Request) (*T, error) {
|
|||||||
if err := binder.Query(req); err != nil {
|
if err := binder.Query(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if slices.Contains([]string{"POST", "PUT", "PATCH"}, strings.ToUpper(r.Method)) {
|
if slices.Contains([]string{"POST", "PUT", "PATCH", "DELETE"}, strings.ToUpper(r.Method)) {
|
||||||
if err := binder.Body(req); err != nil {
|
if err := binder.Body(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error {
|
|||||||
return s.settingRepo.UpdatePanel(ver, url, checksum)
|
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 {
|
func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error {
|
||||||
user := new(biz.User)
|
user := new(biz.User)
|
||||||
if err := app.Orm.Where("id", 1).First(user).Error; err != nil {
|
if err := app.Orm.Where("id", 1).First(user).Error; err != nil {
|
||||||
@@ -349,17 +353,10 @@ func (s *CliService) Port(ctx context.Context, cmd *cli.Command) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error {
|
func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error {
|
||||||
var ports []uint
|
|
||||||
for _, port := range cmd.IntSlice("ports") {
|
|
||||||
if port < 1 || port > 65535 {
|
|
||||||
return fmt.Errorf("端口范围错误")
|
|
||||||
}
|
|
||||||
ports = append(ports, uint(port))
|
|
||||||
}
|
|
||||||
req := &request.WebsiteCreate{
|
req := &request.WebsiteCreate{
|
||||||
Name: cmd.String("name"),
|
Name: cmd.String("name"),
|
||||||
Domains: cmd.StringSlice("domains"),
|
Domains: cmd.StringSlice("domains"),
|
||||||
Ports: ports,
|
Listens: cmd.StringSlice("listens"),
|
||||||
Path: cmd.String("path"),
|
Path: cmd.String("path"),
|
||||||
PHP: int(cmd.Int("php")),
|
PHP: int(cmd.Int("php")),
|
||||||
DB: false,
|
DB: false,
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-rat/chix"
|
"github.com/go-rat/chix"
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
|
"github.com/shirou/gopsutil/disk"
|
||||||
|
"github.com/shirou/gopsutil/host"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
"github.com/TheTNB/panel/internal/app"
|
"github.com/TheTNB/panel/internal/app"
|
||||||
"github.com/TheTNB/panel/internal/biz"
|
"github.com/TheTNB/panel/internal/biz"
|
||||||
"github.com/TheTNB/panel/internal/data"
|
"github.com/TheTNB/panel/internal/data"
|
||||||
|
"github.com/TheTNB/panel/internal/http/request"
|
||||||
"github.com/TheTNB/panel/pkg/api"
|
"github.com/TheTNB/panel/pkg/api"
|
||||||
"github.com/TheTNB/panel/pkg/db"
|
"github.com/TheTNB/panel/pkg/db"
|
||||||
"github.com/TheTNB/panel/pkg/shell"
|
"github.com/TheTNB/panel/pkg/shell"
|
||||||
@@ -21,7 +25,7 @@ import (
|
|||||||
"github.com/TheTNB/panel/pkg/types"
|
"github.com/TheTNB/panel/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InfoService struct {
|
type DashboardService struct {
|
||||||
api *api.API
|
api *api.API
|
||||||
taskRepo biz.TaskRepo
|
taskRepo biz.TaskRepo
|
||||||
websiteRepo biz.WebsiteRepo
|
websiteRepo biz.WebsiteRepo
|
||||||
@@ -30,8 +34,8 @@ type InfoService struct {
|
|||||||
cronRepo biz.CronRepo
|
cronRepo biz.CronRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInfoService() *InfoService {
|
func NewDashboardService() *DashboardService {
|
||||||
return &InfoService{
|
return &DashboardService{
|
||||||
api: api.NewAPI(app.Version),
|
api: api.NewAPI(app.Version),
|
||||||
taskRepo: data.NewTaskRepo(),
|
taskRepo: data.NewTaskRepo(),
|
||||||
websiteRepo: data.NewWebsiteRepo(),
|
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)
|
name, _ := s.settingRepo.Get(biz.SettingKeyName)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
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()
|
apps, err := s.appRepo.GetHomeShow()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(w, http.StatusInternalServerError, "获取首页应用失败")
|
Error(w, http.StatusInternalServerError, "获取首页应用失败: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Success(w, apps)
|
Success(w, apps)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InfoService) Realtime(w http.ResponseWriter, r *http.Request) {
|
func (s *DashboardService) Current(w http.ResponseWriter, r *http.Request) {
|
||||||
Success(w, tools.GetMonitoringInfo())
|
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) {
|
func (s *DashboardService) SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
monitorInfo := tools.GetMonitoringInfo()
|
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{
|
Success(w, chix.M{
|
||||||
"os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion,
|
"procs": hostInfo.Procs,
|
||||||
"uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400),
|
"hostname": hostInfo.Hostname,
|
||||||
"panel_version": app.Version,
|
"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()
|
websiteCount, err := s.websiteRepo.Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(w, http.StatusInternalServerError, "获取网站数量失败")
|
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%")
|
mysqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "mysql%")
|
||||||
postgresqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "postgresql%")
|
postgresqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "postgresql%")
|
||||||
php, _ := s.appRepo.GetInstalledAll("slug like ?", "php%")
|
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
|
current := app.Version
|
||||||
latest, err := s.api.LatestVersion()
|
latest, err := s.api.LatestVersion()
|
||||||
if err != nil {
|
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
|
current := app.Version
|
||||||
latest, err := s.api.LatestVersion()
|
latest, err := s.api.LatestVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -261,21 +311,21 @@ func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
versions, err := s.api.IntermediateVersions()
|
versions, err := s.api.IntermediateVersions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(w, http.StatusInternalServerError, "获取更新信息失败:%v", err)
|
Error(w, http.StatusInternalServerError, "获取升级信息失败:%v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Success(w, versions)
|
Success(w, versions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) {
|
func (s *DashboardService) Update(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.taskRepo.HasRunningTask() {
|
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
|
||||||
Error(w, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
|
Error(w, http.StatusForbidden, "离线模式下无法升级")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE)").Error; err != nil {
|
|
||||||
types.Status = types.StatusFailed
|
if s.taskRepo.HasRunningTask() {
|
||||||
Error(w, http.StatusInternalServerError, "面板数据库异常,已终止操作:%v", err)
|
Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止升级,请稍后再试")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,21 +342,21 @@ func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
ver, url, checksum := panel.Version, download.URL, download.Checksum
|
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 {
|
if err = s.settingRepo.UpdatePanel(ver, url, checksum); err != nil {
|
||||||
types.Status = types.StatusFailed
|
app.Status = app.StatusFailed
|
||||||
Error(w, http.StatusInternalServerError, "%v", err)
|
Error(w, http.StatusInternalServerError, "%v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
types.Status = types.StatusNormal
|
app.Status = app.StatusNormal
|
||||||
Success(w, nil)
|
Success(w, nil)
|
||||||
tools.RestartPanel()
|
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() {
|
if s.taskRepo.HasRunningTask() {
|
||||||
Error(w, http.StatusInternalServerError, "当前有任务正在执行,禁止重启")
|
Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止重启,请稍后再试")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ type FirewallService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewFirewallService() *FirewallService {
|
func NewFirewallService() *FirewallService {
|
||||||
|
|
||||||
return &FirewallService{
|
return &FirewallService{
|
||||||
firewall: firewall.NewFirewall(),
|
firewall: firewall.NewFirewall(),
|
||||||
}
|
}
|
||||||
@@ -74,13 +73,15 @@ func (s *FirewallService) GetRules(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallService) CreateRule(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 {
|
if err != nil {
|
||||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||||
return
|
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: req.Protocol, Address: req.Address, Strategy: req.Strategy, Direction: req.Direction,
|
||||||
|
}, firewall.OperationAdd); err != nil {
|
||||||
Error(w, http.StatusInternalServerError, "%v", err)
|
Error(w, http.StatusInternalServerError, "%v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -89,13 +90,15 @@ func (s *FirewallService) CreateRule(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallService) DeleteRule(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 {
|
if err != nil {
|
||||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||||
return
|
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: req.Protocol, Address: req.Address, Strategy: req.Strategy, Direction: req.Direction,
|
||||||
|
}, firewall.OperationRemove); err != nil {
|
||||||
Error(w, http.StatusInternalServerError, "%v", err)
|
Error(w, http.StatusInternalServerError, "%v", err)
|
||||||
return
|
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.Load1 = append(list.Load.Load1, monitor.Info.Load.Load1)
|
||||||
list.Load.Load5 = append(list.Load.Load5, monitor.Info.Load.Load5)
|
list.Load.Load5 = append(list.Load.Load5, monitor.Info.Load.Load5)
|
||||||
list.Load.Load15 = append(list.Load.Load15, monitor.Info.Load.Load15)
|
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.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.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))
|
list.SWAP.Used = append(list.SWAP.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ func (c *Client) UseHTTP(conf, path string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObtainSSL 签发 SSL 证书
|
// ObtainCertificate 签发 SSL 证书
|
||||||
func (c *Client) ObtainSSL(ctx context.Context, domains []string, keyType KeyType) (Certificate, error) {
|
func (c *Client) ObtainCertificate(ctx context.Context, domains []string, keyType KeyType) (Certificate, error) {
|
||||||
certPrivateKey, err := generatePrivateKey(keyType)
|
certPrivateKey, err := generatePrivateKey(keyType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Certificate{}, err
|
return Certificate{}, err
|
||||||
@@ -76,12 +76,12 @@ func (c *Client) ObtainSSL(ctx context.Context, domains []string, keyType KeyTyp
|
|||||||
return Certificate{}, err
|
return Certificate{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cert := c.selectPreferredChain(certs)
|
crt := c.selectPreferredChain(certs)
|
||||||
return Certificate{PrivateKey: pemPrivateKey, Certificate: cert}, nil
|
return Certificate{PrivateKey: pemPrivateKey, Certificate: crt}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObtainSSLManual 手动验证 SSL 证书
|
// ObtainCertificateManual 手动验证 SSL 证书
|
||||||
func (c *Client) ObtainSSLManual() (Certificate, error) {
|
func (c *Client) ObtainCertificateManual() (Certificate, error) {
|
||||||
// 发送信号,开始验证
|
// 发送信号,开始验证
|
||||||
c.controlChan <- struct{}{}
|
c.controlChan <- struct{}{}
|
||||||
// 等待验证完成
|
// 等待验证完成
|
||||||
@@ -94,20 +94,20 @@ func (c *Client) ObtainSSLManual() (Certificate, error) {
|
|||||||
return data.(Certificate), nil
|
return data.(Certificate), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenewSSL 续签 SSL 证书
|
// RenewCertificate 续签 SSL 证书
|
||||||
func (c *Client) RenewSSL(ctx context.Context, certUrl string, domains []string, keyType KeyType) (Certificate, error) {
|
func (c *Client) RenewCertificate(ctx context.Context, certUrl string, domains []string, keyType KeyType) (Certificate, error) {
|
||||||
_, err := c.zClient.GetCertificateChain(ctx, c.Account, certUrl)
|
_, err := c.zClient.GetCertificateChain(ctx, c.Account, certUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Certificate{}, err
|
return Certificate{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.ObtainSSL(ctx, domains, keyType)
|
return c.ObtainCertificate(ctx, domains, keyType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDNSRecords 获取 DNS 解析(手动设置)
|
// GetDNSRecords 获取 DNS 解析(手动设置)
|
||||||
func (c *Client) GetDNSRecords(ctx context.Context, domains []string, keyType KeyType) ([]DNSRecord, error) {
|
func (c *Client) GetDNSRecords(ctx context.Context, domains []string, keyType KeyType) ([]DNSRecord, error) {
|
||||||
go func(ctx context.Context, domains []string, keyType KeyType) {
|
go func(ctx context.Context, domains []string, keyType KeyType) {
|
||||||
certs, err := c.ObtainSSL(ctx, domains, keyType)
|
certs, err := c.ObtainCertificate(ctx, domains, keyType)
|
||||||
// 将证书和错误信息发送到 dataChan
|
// 将证书和错误信息发送到 dataChan
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.dataChan <- err
|
c.dataChan <- err
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ func (s *ClientTestSuite) TestObtainSSL() {
|
|||||||
|
|
||||||
time.Sleep(2 * time.Minute)
|
time.Sleep(2 * time.Minute)
|
||||||
|
|
||||||
ssl, err := client.ObtainSSLManual()*/
|
ssl, err := client.ObtainCertificateManual()*/
|
||||||
ssl, err := client.ObtainSSL(ctx, []string{"*.haozi.net", "haozi.net"}, KeyEC256)
|
ssl, err := client.ObtainCertificate(ctx, []string{"*.haozi.net", "haozi.net"}, KeyEC256)
|
||||||
s.Error(err)
|
s.Error(err)
|
||||||
s.NotNil(ssl)
|
s.NotNil(ssl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error {
|
|||||||
}
|
}
|
||||||
`, challenge.Token, challenge.KeyAuthorization)
|
`, challenge.Token, challenge.KeyAuthorization)
|
||||||
if err = os.WriteFile(s.conf, []byte(conf), 0644); err != nil {
|
if err = os.WriteFile(s.conf, []byte(conf), 0644); err != nil {
|
||||||
return fmt.Errorf("无法写入OpenResty配置文件: %w", err)
|
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")
|
_, err = shell.Execf("nginx -t")
|
||||||
return fmt.Errorf("无法重载OpenResty: %w", err)
|
return fmt.Errorf("无法重载Nginx: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
package firewall
|
package firewall
|
||||||
|
|
||||||
type FireInfo struct {
|
type FireInfo struct {
|
||||||
Family string `json:"family"` // ipv4 ipv6
|
Family string `json:"family"` // ipv4 ipv6
|
||||||
Address string `json:"address"`
|
Address string `json:"address"` // 源地址或目标地址
|
||||||
Port uint `json:"port"` // 1-65535
|
PortStart uint `json:"port_start"` // 1-65535
|
||||||
Protocol string `json:"protocol"` // tcp udp tcp/udp
|
PortEnd uint `json:"port_end"` // 1-65535
|
||||||
Strategy string `json:"strategy"` // accept drop
|
Protocol string `json:"protocol"` // tcp udp tcp/udp
|
||||||
|
Strategy string `json:"strategy"` // accept drop reject
|
||||||
|
Direction string `json:"direction"` // in out 入站或出站
|
||||||
|
}
|
||||||
|
|
||||||
Num string `json:"num"`
|
type FireForwardInfo struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port uint `json:"port"` // 1-65535
|
||||||
|
Protocol string `json:"protocol"` // tcp udp tcp/udp
|
||||||
TargetIP string `json:"targetIP"`
|
TargetIP string `json:"targetIP"`
|
||||||
TargetPort string `json:"targetPort"` // 1-65535
|
TargetPort string `json:"targetPort"` // 1-65535
|
||||||
|
|
||||||
UsedStatus string `json:"usedStatus"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Forward struct {
|
type Forward struct {
|
||||||
Num string `json:"num"`
|
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
Port uint `json:"port"` // 1-65535
|
Port uint `json:"port"` // 1-65535
|
||||||
TargetIP string `json:"targetIP"`
|
TargetIP string `json:"targetIP"`
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -16,8 +17,8 @@ import (
|
|||||||
type Operation string
|
type Operation string
|
||||||
|
|
||||||
var (
|
var (
|
||||||
OperationAdd Operation = "add"
|
OperationAdd Operation = "add"
|
||||||
OperationDel Operation = "remove"
|
OperationRemove Operation = "remove"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Firewall struct {
|
type Firewall struct {
|
||||||
@@ -28,7 +29,7 @@ type Firewall struct {
|
|||||||
func NewFirewall() *Firewall {
|
func NewFirewall() *Firewall {
|
||||||
firewall := &Firewall{
|
firewall := &Firewall{
|
||||||
forwardListRegex: regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`),
|
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="([^"]+)")?.*?(accept|drop|reject)$`),
|
||||||
}
|
}
|
||||||
|
|
||||||
return firewall
|
return firewall
|
||||||
@@ -58,13 +59,23 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
|
|||||||
if len(port) == 0 {
|
if len(port) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var itemPort FireInfo
|
var item FireInfo
|
||||||
if strings.Contains(port, "/") {
|
if strings.Contains(port, "/") {
|
||||||
itemPort.Port = cast.ToUint(strings.Split(port, "/")[0])
|
ruleItem := strings.Split(port, "/")
|
||||||
itemPort.Protocol = strings.Split(port, "/")[1]
|
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 = ruleItem[1]
|
||||||
}
|
}
|
||||||
itemPort.Strategy = "accept"
|
item.Family = "ipv4"
|
||||||
data = append(data, itemPort)
|
item.Strategy = "accept"
|
||||||
|
item.Direction = "in"
|
||||||
|
data = append(data, item)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
@@ -73,7 +84,6 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data = append(data, rich...)
|
data = append(data, rich...)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -81,13 +91,13 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
|
|||||||
return data, nil
|
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")
|
out, err := shell.Execf("firewall-cmd --zone=public --list-forward-ports")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []FireInfo
|
var data []FireForwardInfo
|
||||||
for _, line := range strings.Split(out, "\n") {
|
for _, line := range strings.Split(out, "\n") {
|
||||||
line = strings.TrimFunc(line, func(r rune) bool {
|
line = strings.TrimFunc(line, func(r rune) bool {
|
||||||
return r <= 32
|
return r <= 32
|
||||||
@@ -100,7 +110,7 @@ func (r *Firewall) ListForward() ([]FireInfo, error) {
|
|||||||
if len(match[4]) == 0 {
|
if len(match[4]) == 0 {
|
||||||
match[4] = "127.0.0.1"
|
match[4] = "127.0.0.1"
|
||||||
}
|
}
|
||||||
data = append(data, FireInfo{
|
data = append(data, FireForwardInfo{
|
||||||
Port: cast.ToUint(match[1]),
|
Port: cast.ToUint(match[1]),
|
||||||
Protocol: match[2],
|
Protocol: match[2],
|
||||||
TargetIP: match[4],
|
TargetIP: match[4],
|
||||||
@@ -124,44 +134,65 @@ func (r *Firewall) ListRichRule() ([]FireInfo, error) {
|
|||||||
if len(rule) == 0 {
|
if len(rule) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if itemRule, err := r.parseRichRule(rule); err == nil {
|
if richRules, err := r.parseRichRule(rule); err == nil {
|
||||||
data = append(data, *itemRule)
|
data = append(data, richRules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Firewall) Port(port FireInfo, operation Operation) error {
|
func (r *Firewall) Port(rule FireInfo, operation Operation) error {
|
||||||
stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d/%s --permanent", operation, port.Port, port.Protocol)
|
if rule.PortEnd == 0 {
|
||||||
if err != nil {
|
rule.PortEnd = rule.PortStart
|
||||||
return fmt.Errorf("%s port %d/%s failed, err: %s", operation, port.Port, port.Protocol, stdout)
|
}
|
||||||
|
if rule.PortStart > rule.PortEnd {
|
||||||
|
return fmt.Errorf("invalid port range: %d-%d", rule.PortStart, rule.PortEnd)
|
||||||
|
}
|
||||||
|
// 不支持的切换使用rich rules
|
||||||
|
if rule.Direction != "in" || rule.Family != "ipv4" || rule.Address != "" || rule.Strategy != "accept" {
|
||||||
|
return r.RichRules(rule, operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = shell.Execf("firewall-cmd --reload")
|
protocols := strings.Split(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 port %d-%d/%s failed, err: %s", operation, rule.PortStart, rule.PortEnd, protocol, stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := shell.Execf("firewall-cmd --reload")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
|
func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
|
||||||
families := strings.Split(rule.Family, "/") // ipv4 ipv6
|
protocols := strings.Split(rule.Protocol, "/")
|
||||||
|
for protocol := range slices.Values(protocols) {
|
||||||
|
var ruleBuilder strings.Builder
|
||||||
|
ruleBuilder.WriteString(fmt.Sprintf(`rule family="%s" `, rule.Family))
|
||||||
|
|
||||||
for _, family := range families {
|
|
||||||
var ruleStr strings.Builder
|
|
||||||
ruleStr.WriteString(fmt.Sprintf(`rule family="%s" `, family))
|
|
||||||
if len(rule.Address) != 0 {
|
if len(rule.Address) != 0 {
|
||||||
ruleStr.WriteString(fmt.Sprintf(`source address="%s" `, rule.Address))
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if rule.Port != 0 {
|
if rule.PortStart != 0 && rule.PortEnd != 0 && (rule.PortStart != 1 && rule.PortEnd != 65535) { // 1-65535是解析出来无端口规则的情况
|
||||||
ruleStr.WriteString(fmt.Sprintf(`port port="%d" `, rule.Port))
|
ruleBuilder.WriteString(fmt.Sprintf(`port port="%d-%d" `, rule.PortStart, rule.PortEnd))
|
||||||
}
|
}
|
||||||
if len(rule.Protocol) != 0 {
|
if operation == OperationRemove && protocol != "" && rule.Protocol != "tcp/udp" { // 删除操作,可以不指定协议
|
||||||
ruleStr.WriteString(fmt.Sprintf(`protocol="%s" `, rule.Protocol))
|
ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol))
|
||||||
|
}
|
||||||
|
if operation == OperationAdd && protocol != "" {
|
||||||
|
ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol))
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleStr.WriteString(rule.Strategy)
|
ruleBuilder.WriteString(rule.Strategy)
|
||||||
_, err := shell.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleStr.String())
|
_, err := shell.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleBuilder.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s rich rules (%s) failed, err: %v", operation, ruleStr.String(), err)
|
return fmt.Errorf("%s rich rules (%s) failed, err: %v", operation, ruleBuilder.String(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,22 +223,46 @@ func (r *Firewall) PortForward(info Forward, operation Operation) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Firewall) parseRichRule(line string) (*FireInfo, error) {
|
func (r *Firewall) parseRichRule(line string) (FireInfo, error) {
|
||||||
itemRule := new(FireInfo)
|
if !r.richRuleRegex.MatchString(line) {
|
||||||
if r.richRuleRegex.MatchString(line) {
|
return FireInfo{}, errors.New("invalid rich rule format")
|
||||||
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]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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: match[5],
|
||||||
|
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 {
|
func (r *Firewall) enableForward() error {
|
||||||
|
|||||||
@@ -191,11 +191,6 @@ func FormatArchiveByPath(path string) (FormatArchive, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TempFile 创建临时文件
|
|
||||||
func TempFile(prefix string) (*os.File, error) {
|
|
||||||
return os.CreateTemp("", prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSymlink 判读是否为软链接
|
// IsSymlink 判读是否为软链接
|
||||||
func IsSymlink(mode os.FileMode) bool {
|
func IsSymlink(mode os.FileMode) bool {
|
||||||
return mode&os.ModeSymlink != 0
|
return mode&os.ModeSymlink != 0
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/TheTNB/panel/pkg/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove 删除文件/目录
|
// Remove 删除文件/目录
|
||||||
@@ -49,17 +51,14 @@ func Empty(path string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Mv(src, dst string) error {
|
func Mv(src, dst string) error {
|
||||||
err := os.Rename(src, dst)
|
if err := os.Rename(src, dst); err != nil {
|
||||||
if err != nil {
|
// 在不同的文件系统中无法使用os.Rename
|
||||||
// 如果在不同的文件系统中移动文件,os.Rename 可能会失败
|
if _, err = shell.Execf(`mv -f '%s' '%s'`, src, dst); err != nil {
|
||||||
err = Cp(src, dst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = os.RemoveAll(src)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cp 复制文件或目录
|
// Cp 复制文件或目录
|
||||||
@@ -147,6 +146,11 @@ func TempDir(prefix string) (string, error) {
|
|||||||
return os.MkdirTemp("", prefix)
|
return os.MkdirTemp("", prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TempFile 创建临时文件
|
||||||
|
func TempFile(dir, prefix string) (*os.File, error) {
|
||||||
|
return os.CreateTemp(dir, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
// ReadDir 读取目录
|
// ReadDir 读取目录
|
||||||
func ReadDir(path string) ([]os.DirEntry, error) {
|
func ReadDir(path string) ([]os.DirEntry, error) {
|
||||||
return os.ReadDir(path)
|
return os.ReadDir(path)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package nginx
|
||||||
|
|
||||||
|
var order = []string{"listen", "server_name", "index", "root",
|
||||||
|
"ssl_certificate", "ssl_certificate_key", "ssl_session_timeout", "ssl_session_cache", "ssl_protocols", "ssl_ciphers", "ssl_prefer_server_ciphers", "ssl_early_data", "ssl_stapling", "ssl_stapling_verify", "ssl_trusted_certificate",
|
||||||
|
"resolver", "error_page", "include", "if", "location", "add_header", "access_log", "error_log"}
|
||||||
|
|
||||||
|
const defaultConf = `server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
root /www/wwwroot/default;
|
||||||
|
# 错误页配置
|
||||||
|
error_page 404 /404.html;
|
||||||
|
include enable-php-0.conf;
|
||||||
|
# 不记录静态文件日志
|
||||||
|
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {
|
||||||
|
expires 30d;
|
||||||
|
access_log /dev/null;
|
||||||
|
error_log /dev/null;
|
||||||
|
}
|
||||||
|
location ~ .*\.(js|css|ttf|otf|woff|woff2|eot)$ {
|
||||||
|
expires 6h;
|
||||||
|
access_log /dev/null;
|
||||||
|
error_log /dev/null;
|
||||||
|
}
|
||||||
|
# 禁止部分敏感目录
|
||||||
|
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
access_log /www/wwwlogs/default.log;
|
||||||
|
error_log /www/wwwlogs/default.log;
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package nginx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Parser) GetListen() ([][]string, error) {
|
||||||
|
directives, err := p.Find("server.listen")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result [][]string
|
||||||
|
for _, dir := range directives {
|
||||||
|
result = append(result, dir.GetParameters())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetServerName() ([]string, error) {
|
||||||
|
directive, err := p.FindOne("server.server_name")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetIndex() ([]string, error) {
|
||||||
|
directive, err := p.FindOne("server.index")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetIndexWithComment() ([]string, []string, error) {
|
||||||
|
directive, err := p.FindOne("server.index")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters(), directive.GetComment(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetRoot() (string, error) {
|
||||||
|
directive, err := p.FindOne("server.root")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(directive.GetParameters()) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters()[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetRootWithComment() (string, []string, error) {
|
||||||
|
directive, err := p.FindOne("server.root")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if len(directive.GetParameters()) == 0 {
|
||||||
|
return "", directive.GetComment(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters()[0], directive.GetComment(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetIncludes() (includes []string, comments [][]string, err error) {
|
||||||
|
directives, err := p.Find("server.include")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range directives {
|
||||||
|
if len(dir.GetParameters()) != 1 {
|
||||||
|
return nil, nil, fmt.Errorf("invalid include directive, expected 1 parameter but got %d", len(dir.GetParameters()))
|
||||||
|
}
|
||||||
|
includes = append(includes, dir.GetParameters()[0])
|
||||||
|
comments = append(comments, dir.GetComment())
|
||||||
|
}
|
||||||
|
|
||||||
|
return includes, comments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetPHP() int {
|
||||||
|
directives, err := p.Find("server.include")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var result int
|
||||||
|
for _, dir := range directives {
|
||||||
|
if slices.ContainsFunc(dir.GetParameters(), func(s string) bool {
|
||||||
|
return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf")
|
||||||
|
}) {
|
||||||
|
_, _ = fmt.Sscanf(dir.GetParameters()[0], "enable-php-%d.conf", &result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetHTTPS() bool {
|
||||||
|
directive, err := p.FindOne("server.ssl_certificate")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(directive.GetParameters()) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetHTTPSProtocols() []string {
|
||||||
|
directive, err := p.FindOne("server.ssl_protocols")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetHTTPSCiphers() string {
|
||||||
|
directive, err := p.FindOne("server.ssl_ciphers")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(directive.GetParameters()) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetOCSP() bool {
|
||||||
|
directive, err := p.FindOne("server.ssl_stapling")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(directive.GetParameters()) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters()[0] == "on"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetHSTS() bool {
|
||||||
|
directives, err := p.Find("server.add_header")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range directives {
|
||||||
|
if slices.Contains(dir.GetParameters(), "Strict-Transport-Security") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetHTTPSRedirect() bool {
|
||||||
|
directives, err := p.Find("server.if")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range directives {
|
||||||
|
for _, dir2 := range dir.GetBlock().GetDirectives() {
|
||||||
|
if dir2.GetName() == "return" && slices.Contains(dir2.GetParameters(), "https://$host$request_uri") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetAccessLog() (string, error) {
|
||||||
|
directive, err := p.FindOne("server.access_log")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(directive.GetParameters()) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters()[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetErrorLog() (string, error) {
|
||||||
|
directive, err := p.FindOne("server.error_log")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(directive.GetParameters()) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return directive.GetParameters()[0], nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package nginx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tufanbarisyildirim/gonginx/config"
|
||||||
|
"github.com/tufanbarisyildirim/gonginx/dumper"
|
||||||
|
"github.com/tufanbarisyildirim/gonginx/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parser Nginx vhost 配置解析器
|
||||||
|
type Parser struct {
|
||||||
|
c *config.Config
|
||||||
|
orderIndex map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParser(str ...string) (*Parser, error) {
|
||||||
|
if len(str) == 0 {
|
||||||
|
str = append(str, defaultConf)
|
||||||
|
}
|
||||||
|
p := parser.NewStringParser(str[0], parser.WithSkipIncludeParsingErr(), parser.WithSkipValidDirectivesErr())
|
||||||
|
c, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderIndex := make(map[string]int)
|
||||||
|
for i, name := range order {
|
||||||
|
orderIndex[name] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Parser{c: c, orderIndex: orderIndex}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Config() *config.Config {
|
||||||
|
return p.c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find 通过表达式查找配置
|
||||||
|
// e.g. Find("server.listen")
|
||||||
|
func (p *Parser) Find(key string) ([]config.IDirective, error) {
|
||||||
|
parts := strings.Split(key, ".")
|
||||||
|
var block *config.Block
|
||||||
|
var ok bool
|
||||||
|
block = p.c.Block
|
||||||
|
for i := 0; i < len(parts)-1; i++ {
|
||||||
|
key = parts[i]
|
||||||
|
directives := block.FindDirectives(key)
|
||||||
|
if len(directives) == 0 {
|
||||||
|
return nil, errors.New("given key not found")
|
||||||
|
}
|
||||||
|
if len(directives) > 1 {
|
||||||
|
return nil, errors.New("multiple directives found")
|
||||||
|
}
|
||||||
|
block, ok = directives[0].GetBlock().(*config.Block)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("block is not *config.Block")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []config.IDirective
|
||||||
|
for _, dir := range block.GetDirectives() {
|
||||||
|
if dir.GetName() == parts[len(parts)-1] {
|
||||||
|
result = append(result, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindOne 通过表达式查找一个配置
|
||||||
|
// e.g. FindOne("server.server_name")
|
||||||
|
func (p *Parser) FindOne(key string) (config.IDirective, error) {
|
||||||
|
directives, err := p.Find(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(directives) == 0 {
|
||||||
|
return nil, errors.New("given key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return directives[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 通过表达式移除配置
|
||||||
|
// e.g. Clear("server.server_name")
|
||||||
|
func (p *Parser) Clear(key string) error {
|
||||||
|
parts := strings.Split(key, ".")
|
||||||
|
last := parts[len(parts)-1]
|
||||||
|
parts = parts[:len(parts)-1]
|
||||||
|
|
||||||
|
var block *config.Block
|
||||||
|
var ok bool
|
||||||
|
block = p.c.Block
|
||||||
|
for i := 0; i < len(parts); i++ {
|
||||||
|
directives := block.FindDirectives(parts[i])
|
||||||
|
if len(directives) == 0 {
|
||||||
|
return errors.New("given key not found")
|
||||||
|
}
|
||||||
|
if len(directives) > 1 {
|
||||||
|
return errors.New("multiple directives found")
|
||||||
|
}
|
||||||
|
block, ok = directives[0].GetBlock().(*config.Block)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("block is not *config.Block")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newDirectives []config.IDirective
|
||||||
|
for _, directive := range block.GetDirectives() {
|
||||||
|
if directive.GetName() != last {
|
||||||
|
newDirectives = append(newDirectives, directive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
block.Directives = newDirectives
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 通过表达式设置配置
|
||||||
|
// e.g. Set("server.server_name", []directive)
|
||||||
|
func (p *Parser) Set(key string, directives []*config.Directive) error {
|
||||||
|
parts := strings.Split(key, ".")
|
||||||
|
|
||||||
|
var block *config.Block
|
||||||
|
var ok bool
|
||||||
|
block = p.c.Block
|
||||||
|
for i := 0; i < len(parts); i++ {
|
||||||
|
sub := block.FindDirectives(parts[i])
|
||||||
|
if len(sub) == 0 {
|
||||||
|
return errors.New("given key not found")
|
||||||
|
}
|
||||||
|
if len(sub) > 1 {
|
||||||
|
return errors.New("multiple directives found")
|
||||||
|
}
|
||||||
|
block, ok = sub[0].GetBlock().(*config.Block)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("block is not *config.Block")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, directive := range directives {
|
||||||
|
directive.SetParent(block)
|
||||||
|
block.Directives = append(block.Directives, directive)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Sort() {
|
||||||
|
p.sortDirectives(p.c.Directives, p.orderIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Dump() string {
|
||||||
|
p.Sort()
|
||||||
|
return dumper.DumpConfig(p.c, dumper.IndentedStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) sortDirectives(directives []config.IDirective, orderIndex map[string]int) {
|
||||||
|
slices.SortFunc(directives, func(a config.IDirective, b config.IDirective) int {
|
||||||
|
if orderIndex[a.GetName()] != orderIndex[b.GetName()] {
|
||||||
|
return orderIndex[a.GetName()] - orderIndex[b.GetName()]
|
||||||
|
}
|
||||||
|
return slices.Compare(a.GetParameters(), b.GetParameters())
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, directive := range directives {
|
||||||
|
if block, ok := directive.GetBlock().(*config.Block); ok {
|
||||||
|
p.sortDirectives(block.Directives, orderIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package nginx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/TheTNB/panel/pkg/io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NginxTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNginxTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &NginxTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestListen() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
listen, err := parser.GetListen()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([][]string{{"80"}}, listen)
|
||||||
|
s.NoError(parser.SetListen([][]string{{"80"}, {"443"}}))
|
||||||
|
listen, err = parser.GetListen()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([][]string{{"80"}, {"443"}}, listen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestServerName() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
serverName, err := parser.GetServerName()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([]string{"localhost"}, serverName)
|
||||||
|
s.NoError(parser.SetServerName([]string{"example.com"}))
|
||||||
|
serverName, err = parser.GetServerName()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([]string{"example.com"}, serverName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestIndex() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
index, err := parser.GetIndex()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([]string{"index.php", "index.html", "index.htm"}, index)
|
||||||
|
s.NoError(parser.SetIndex([]string{"index.html", "index.htm"}))
|
||||||
|
index, err = parser.GetIndex()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([]string{"index.html", "index.htm"}, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestIndexWithComment() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
index, comment, err := parser.GetIndexWithComment()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([]string{"index.php", "index.html", "index.htm"}, index)
|
||||||
|
s.Equal([]string(nil), comment)
|
||||||
|
s.NoError(parser.SetIndexWithComment([]string{"index.html", "index.htm"}, []string{"# 测试"}))
|
||||||
|
index, comment, err = parser.GetIndexWithComment()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([]string{"index.html", "index.htm"}, index)
|
||||||
|
s.Equal([]string{"# 测试"}, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestRoot() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
root, err := parser.GetRoot()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal("/www/wwwroot/default", root)
|
||||||
|
s.NoError(parser.SetRoot("/www/wwwroot/test"))
|
||||||
|
root, err = parser.GetRoot()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal("/www/wwwroot/test", root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestRootWithComment() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
root, comment, err := parser.GetRootWithComment()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal("/www/wwwroot/default", root)
|
||||||
|
s.Equal([]string(nil), comment)
|
||||||
|
s.NoError(parser.SetRootWithComment("/www/wwwroot/test", []string{"# 测试"}))
|
||||||
|
root, comment, err = parser.GetRootWithComment()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal("/www/wwwroot/test", root)
|
||||||
|
s.Equal([]string{"# 测试"}, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestIncludes() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
includes, comments, err := parser.GetIncludes()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([]string{"enable-php-0.conf"}, includes)
|
||||||
|
s.Equal([][]string{[]string(nil)}, comments)
|
||||||
|
s.NoError(parser.SetIncludes([]string{"/www/server/vhost/rewrite/default.conf"}, nil))
|
||||||
|
includes, comments, err = parser.GetIncludes()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([]string{"/www/server/vhost/rewrite/default.conf"}, includes)
|
||||||
|
s.Equal([][]string{[]string(nil)}, comments)
|
||||||
|
s.NoError(parser.SetIncludes([]string{"/www/server/vhost/rewrite/test.conf"}, [][]string{{"# 伪静态规则测试"}}))
|
||||||
|
includes, comments, err = parser.GetIncludes()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal([]string{"/www/server/vhost/rewrite/test.conf"}, includes)
|
||||||
|
s.Equal([][]string{{"# 伪静态规则测试"}}, comments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestPHP() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal(0, parser.GetPHP())
|
||||||
|
s.NoError(parser.SetPHP(80))
|
||||||
|
s.Equal(80, parser.GetPHP())
|
||||||
|
s.NoError(parser.SetPHP(0))
|
||||||
|
s.Equal(0, parser.GetPHP())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestHTTP() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
expect, err := io.Read("testdata/http.conf")
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal(expect, parser.Dump())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestHTTPS() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
s.False(parser.GetHTTPS())
|
||||||
|
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)
|
||||||
|
s.Equal(expect, parser.Dump())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestHTTPSProtocols() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestHTTPSCiphers() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestOCSP() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
s.NoError(err)
|
||||||
|
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())
|
||||||
|
s.NoError(parser.SetOCSP(true))
|
||||||
|
s.True(parser.GetOCSP())
|
||||||
|
s.NoError(parser.SetOCSP(false))
|
||||||
|
s.False(parser.GetOCSP())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestHSTS() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
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())
|
||||||
|
s.NoError(parser.SetHSTS(true))
|
||||||
|
s.True(parser.GetHSTS())
|
||||||
|
s.NoError(parser.SetHSTS(false))
|
||||||
|
s.False(parser.GetHSTS())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestHTTPSRedirect() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
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())
|
||||||
|
s.NoError(parser.SetHTTPRedirect(true))
|
||||||
|
s.True(parser.GetHTTPSRedirect())
|
||||||
|
s.NoError(parser.SetHTTPRedirect(false))
|
||||||
|
s.False(parser.GetHTTPSRedirect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestAccessLog() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
log, err := parser.GetAccessLog()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal("/www/wwwlogs/default.log", log)
|
||||||
|
s.NoError(parser.SetAccessLog("/www/wwwlogs/access.log"))
|
||||||
|
log, err = parser.GetAccessLog()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal("/www/wwwlogs/access.log", log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NginxTestSuite) TestErrorLog() {
|
||||||
|
parser, err := NewParser()
|
||||||
|
s.NoError(err)
|
||||||
|
log, err := parser.GetErrorLog()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal("/www/wwwlogs/default.log", log)
|
||||||
|
s.NoError(parser.SetErrorLog("/www/wwwlogs/error.log"))
|
||||||
|
log, err = parser.GetErrorLog()
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal("/www/wwwlogs/error.log", log)
|
||||||
|
}
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
package nginx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tufanbarisyildirim/gonginx/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Parser) SetListen(listen [][]string) error {
|
||||||
|
var directives []*config.Directive
|
||||||
|
for _, l := range listen {
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Name: "listen",
|
||||||
|
Parameters: l,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.Clear("server.listen"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", directives)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetServerName(serverName []string) error {
|
||||||
|
if err := p.Clear("server.server_name"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "server_name",
|
||||||
|
Parameters: serverName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetIndex(index []string) error {
|
||||||
|
if err := p.Clear("server.index"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "index",
|
||||||
|
Parameters: index,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetIndexWithComment(index []string, comment []string) error {
|
||||||
|
if err := p.Clear("server.index"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "index",
|
||||||
|
Parameters: index,
|
||||||
|
Comment: comment,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetRoot(root string) error {
|
||||||
|
if err := p.Clear("server.root"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "root",
|
||||||
|
Parameters: []string{root},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetRootWithComment(root string, comment []string) error {
|
||||||
|
if err := p.Clear("server.root"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "root",
|
||||||
|
Parameters: []string{root},
|
||||||
|
Comment: comment,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetIncludes(includes []string, comments [][]string) error {
|
||||||
|
if err := p.Clear("server.include"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var directives []*config.Directive
|
||||||
|
for i, item := range includes {
|
||||||
|
var comment []string
|
||||||
|
if i < len(comments) {
|
||||||
|
comment = comments[i]
|
||||||
|
}
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Name: "include",
|
||||||
|
Parameters: []string{item},
|
||||||
|
Comment: comment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", directives)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetPHP(php int) error {
|
||||||
|
old, err := p.Find("server.include")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.Clear("server.include"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var directives []*config.Directive
|
||||||
|
var foundFlag bool
|
||||||
|
for _, item := range old {
|
||||||
|
// 查找enable-php的配置
|
||||||
|
if slices.ContainsFunc(item.GetParameters(), func(s string) bool {
|
||||||
|
return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf")
|
||||||
|
}) {
|
||||||
|
foundFlag = true
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Name: item.GetName(),
|
||||||
|
Parameters: []string{fmt.Sprintf("enable-php-%d.conf", php)},
|
||||||
|
Comment: item.GetComment(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 其余的原样保留
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Name: item.GetName(),
|
||||||
|
Parameters: item.GetParameters(),
|
||||||
|
Comment: item.GetComment(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到enable-php的配置,直接添加一个
|
||||||
|
if !foundFlag {
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Name: "include",
|
||||||
|
Parameters: []string{fmt.Sprintf("enable-php-%d.conf", php)},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", directives)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) ClearSetHTTPS() error {
|
||||||
|
if err := p.Clear("server.ssl_certificate"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.Clear("server.ssl_certificate_key"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.Clear("server.ssl_session_timeout"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.Clear("server.ssl_session_cache"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.Clear("server.ssl_protocols"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.Clear("server.ssl_ciphers"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.Clear("server.ssl_prefer_server_ciphers"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.Clear("server.ssl_early_data"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetHTTPS(cert, key string) error {
|
||||||
|
if err := p.ClearSetHTTPS(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "ssl_certificate",
|
||||||
|
Parameters: []string{cert},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ssl_certificate_key",
|
||||||
|
Parameters: []string{key},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ssl_session_timeout",
|
||||||
|
Parameters: []string{"1d"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ssl_session_cache",
|
||||||
|
Parameters: []string{"shared:SSL:10m"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ssl_protocols",
|
||||||
|
Parameters: []string{"TLSv1.2", "TLSv1.3"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ssl_ciphers",
|
||||||
|
Parameters: []string{"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"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ssl_prefer_server_ciphers",
|
||||||
|
Parameters: []string{"off"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ssl_early_data",
|
||||||
|
Parameters: []string{"on"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetHTTPSProtocols(protocols []string) error {
|
||||||
|
if err := p.Clear("server.ssl_protocols"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "ssl_protocols",
|
||||||
|
Parameters: protocols,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetHTTPSCiphers(ciphers string) error {
|
||||||
|
if err := p.Clear("server.ssl_ciphers"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "ssl_ciphers",
|
||||||
|
Parameters: []string{ciphers},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetOCSP(ocsp bool) error {
|
||||||
|
if err := p.Clear("server.ssl_stapling"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.Clear("server.ssl_stapling_verify"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ocsp {
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "ssl_stapling",
|
||||||
|
Parameters: []string{"on"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ssl_stapling_verify",
|
||||||
|
Parameters: []string{"on"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetHSTS(hsts bool) error {
|
||||||
|
old, err := p.Find("server.add_header")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.Clear("server.add_header"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var directives []*config.Directive
|
||||||
|
var foundFlag bool
|
||||||
|
for _, dir := range old {
|
||||||
|
if slices.Contains(dir.GetParameters(), "Strict-Transport-Security") {
|
||||||
|
foundFlag = true
|
||||||
|
if hsts {
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Name: dir.GetName(),
|
||||||
|
Parameters: []string{"Strict-Transport-Security", "max-age=31536000"},
|
||||||
|
Comment: dir.GetComment(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Name: dir.GetName(),
|
||||||
|
Parameters: dir.GetParameters(),
|
||||||
|
Comment: dir.GetComment(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundFlag && hsts {
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Name: "add_header",
|
||||||
|
Parameters: []string{"Strict-Transport-Security", "max-age=31536000"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", directives)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetHTTPRedirect(httpRedirect bool) error {
|
||||||
|
// if 重定向
|
||||||
|
ifs, err := p.Find("server.if")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.Clear("server.if"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var directives []*config.Directive
|
||||||
|
var foundFlag bool
|
||||||
|
for _, dir := range ifs { // 所有 if
|
||||||
|
if !httpRedirect {
|
||||||
|
if len(dir.GetParameters()) == 3 && dir.GetParameters()[0] == "($scheme" && dir.GetParameters()[1] == "=" && dir.GetParameters()[2] == "http)" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ifDirectives []config.IDirective
|
||||||
|
for _, dir2 := range dir.GetBlock().GetDirectives() { // 每个 if 中所有指令
|
||||||
|
if !httpRedirect {
|
||||||
|
// 不启用http重定向,则判断并移除特定的return指令
|
||||||
|
if dir2.GetName() != "return" && !slices.Contains(dir2.GetParameters(), "https://$host$request_uri") {
|
||||||
|
ifDirectives = append(ifDirectives, dir2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 启用http重定向,需要检查防止重复添加
|
||||||
|
if dir2.GetName() == "return" && slices.Contains(dir2.GetParameters(), "https://$host$request_uri") {
|
||||||
|
foundFlag = true
|
||||||
|
}
|
||||||
|
ifDirectives = append(ifDirectives, dir2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 写回 if 指令
|
||||||
|
if block, ok := dir.GetBlock().(*config.Block); ok {
|
||||||
|
block.Directives = ifDirectives
|
||||||
|
}
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Block: dir.GetBlock(),
|
||||||
|
Name: dir.GetName(),
|
||||||
|
Parameters: dir.GetParameters(),
|
||||||
|
Comment: dir.GetComment(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundFlag && httpRedirect {
|
||||||
|
ifDir := &config.Directive{
|
||||||
|
Name: "if",
|
||||||
|
Block: &config.Block{},
|
||||||
|
Parameters: []string{"($scheme", "=", "http)"},
|
||||||
|
}
|
||||||
|
redirectDir := &config.Directive{
|
||||||
|
Name: "return",
|
||||||
|
Parameters: []string{"308", "https://$host$request_uri"},
|
||||||
|
}
|
||||||
|
redirectDir.SetParent(ifDir.GetBlock())
|
||||||
|
ifBlock := ifDir.GetBlock().(*config.Block)
|
||||||
|
ifBlock.Directives = append(ifBlock.Directives, redirectDir)
|
||||||
|
directives = append(directives, ifDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.Set("server", directives); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// error_page 497 重定向
|
||||||
|
directives = nil
|
||||||
|
errorPages, err := p.Find("server.error_page")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.Clear("server.error_page"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var found497 bool
|
||||||
|
for _, dir := range errorPages {
|
||||||
|
if !httpRedirect {
|
||||||
|
// 不启用https重定向,则判断并移除特定的return指令
|
||||||
|
if !slices.Contains(dir.GetParameters(), "497") && !slices.Contains(dir.GetParameters(), "https://$host:$server_port$request_uri") {
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Block: dir.GetBlock(),
|
||||||
|
Name: dir.GetName(),
|
||||||
|
Parameters: dir.GetParameters(),
|
||||||
|
Comment: dir.GetComment(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 启用https重定向,需要检查防止重复添加
|
||||||
|
if slices.Contains(dir.GetParameters(), "497") && slices.Contains(dir.GetParameters(), "https://$host:$server_port$request_uri") {
|
||||||
|
found497 = true
|
||||||
|
}
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Block: dir.GetBlock(),
|
||||||
|
Name: dir.GetName(),
|
||||||
|
Parameters: dir.GetParameters(),
|
||||||
|
Comment: dir.GetComment(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found497 && httpRedirect {
|
||||||
|
directives = append(directives, &config.Directive{
|
||||||
|
Name: "error_page",
|
||||||
|
Parameters: []string{"497", "=307", "https://$host:$server_port$request_uri"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", directives)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetAccessLog(accessLog string) error {
|
||||||
|
if err := p.Clear("server.access_log"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "access_log",
|
||||||
|
Parameters: []string{accessLog},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetErrorLog(errorLog string) error {
|
||||||
|
if err := p.Clear("server.error_log"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Set("server", []*config.Directive{
|
||||||
|
{
|
||||||
|
Name: "error_log",
|
||||||
|
Parameters: []string{errorLog},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
root /www/wwwroot/default;
|
||||||
|
# 错误页配置
|
||||||
|
error_page 404 /404.html;
|
||||||
|
include enable-php-0.conf;
|
||||||
|
# 不记录静态文件日志
|
||||||
|
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {
|
||||||
|
expires 30d;
|
||||||
|
access_log /dev/null;
|
||||||
|
error_log /dev/null;
|
||||||
|
}
|
||||||
|
location ~ .*\.(js|css|ttf|otf|woff|woff2|eot)$ {
|
||||||
|
expires 6h;
|
||||||
|
access_log /dev/null;
|
||||||
|
error_log /dev/null;
|
||||||
|
}
|
||||||
|
# 禁止部分敏感目录
|
||||||
|
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
access_log /www/wwwlogs/default.log;
|
||||||
|
error_log /www/wwwlogs/default.log;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
root /www/wwwroot/default;
|
||||||
|
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;
|
||||||
|
ssl_ciphers 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;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_early_data on;
|
||||||
|
# 错误页配置
|
||||||
|
error_page 404 /404.html;
|
||||||
|
include enable-php-0.conf;
|
||||||
|
# 不记录静态文件日志
|
||||||
|
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {
|
||||||
|
expires 30d;
|
||||||
|
access_log /dev/null;
|
||||||
|
error_log /dev/null;
|
||||||
|
}
|
||||||
|
location ~ .*\.(js|css|ttf|otf|woff|woff2|eot)$ {
|
||||||
|
expires 6h;
|
||||||
|
access_log /dev/null;
|
||||||
|
error_log /dev/null;
|
||||||
|
}
|
||||||
|
# 禁止部分敏感目录
|
||||||
|
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
access_log /www/wwwlogs/default.log;
|
||||||
|
error_log /www/wwwlogs/default.log;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package punycode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncodeDomain 将 Unicode 域名编码为 Punycode
|
||||||
|
func EncodeDomain(domain string) (string, error) {
|
||||||
|
ascii, err := idna.ToASCII(domain)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("domain encode failed: %w", err)
|
||||||
|
}
|
||||||
|
return ascii, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeDomains 将 Unicode 域名列表编码为 Punycode
|
||||||
|
func EncodeDomains(domain []string) (encoded []string, err error) {
|
||||||
|
var punycode string
|
||||||
|
for item := range slices.Values(domain) {
|
||||||
|
punycode, err = EncodeDomain(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encoded = append(encoded, punycode)
|
||||||
|
}
|
||||||
|
return encoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeDomain 将 Punycode 域名解码为 Unicode 域名
|
||||||
|
func DecodeDomain(punycodeDomain string) (string, error) {
|
||||||
|
unicode, err := idna.ToUnicode(punycodeDomain)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("domain decode failed: %w", err)
|
||||||
|
}
|
||||||
|
return unicode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeDomains 将 Punycode 域名列表解码为 Unicode 域名
|
||||||
|
func DecodeDomains(punycode []string) (decoded []string, err error) {
|
||||||
|
var unicode string
|
||||||
|
for item := range slices.Values(punycode) {
|
||||||
|
unicode, err = DecodeDomain(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
decoded = append(decoded, unicode)
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||