Compare commits

...

121 Commits

Author SHA1 Message Date
耗子 f7c586e05b feat: 发布v2.3.14 2024-10-26 04:46:16 +08:00
耗子 96c78b6be9 fix: lint 2024-10-26 04:44:17 +08:00
耗子 46ae37a4a1 feat: 优化自动签发证书 2024-10-26 04:39:22 +08:00
耗子 d56b41bfef feat: 为quic自动添加alt-svc 2024-10-26 03:32:33 +08:00
耗子 68beffff22 Merge remote-tracking branch 'origin/main' 2024-10-26 02:53:21 +08:00
耗子 a774253ec3 feat: 优化证书列表账号显示 2024-10-26 02:53:15 +08:00
耗子 9dfc1d3058 Revert "feat: 尝试改善日志性能"
This reverts commit 5ac43df821.
2024-10-26 02:33:18 +08:00
耗子 049f708cae feat: 优化实时日志为500行 2024-10-26 02:32:57 +08:00
renovate[bot] b749a8599f chore(deps): Update dependency @types/node to v20.17.1 (#299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-26 02:30:07 +08:00
耗子 95cf05879d feat: 支持一键签发证书 2024-10-26 02:28:52 +08:00
耗子 9ac5ae4968 feat: nginx解析器支持设置alt-svc 2024-10-26 01:37:51 +08:00
耗子 2491f34b3f fix: lint 2024-10-26 01:23:25 +08:00
耗子 302d0f1bec fix: 防火墙关闭状态下ping状态报错 2024-10-26 01:23:12 +08:00
耗子 5ac43df821 feat: 尝试改善日志性能 2024-10-25 22:31:38 +08:00
耗子 8a1e7ef88b Merge remote-tracking branch 'origin/main' 2024-10-25 21:29:09 +08:00
耗子 b0a4df7ca6 fix: 创建证书账号/dns后刷新异步数据 2024-10-25 21:29:03 +08:00
renovate[bot] 70f41b87ea chore(deps): Update dependency pinia-plugin-persistedstate to v4.1.2 (#297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-25 18:16:20 +08:00
耗子 ea96634318 fix: lint 2024-10-25 17:21:27 +08:00
耗子 cfff459673 feat: 移除color包 2024-10-25 17:06:04 +08:00
耗子 bba50692b3 fix: test 2024-10-25 16:53:08 +08:00
耗子 595cb03c67 workflow: 去掉warp actions,太慢了 2024-10-25 16:49:32 +08:00
耗子 fa5817febd fix: lint 2024-10-25 16:36:12 +08:00
耗子 9a9fdbdca6 workflow: 支持ipv6网络 2024-10-25 16:32:47 +08:00
耗子 2225e59ca6 fix: 修改权限不使用sudo 2024-10-25 02:43:33 +08:00
耗子 5dce30523b feat: 支持同时输出ipv4 v6地址 2024-10-25 02:41:58 +08:00
耗子 965c39ee08 feat: 创建网站不允许使用443端口 2024-10-25 02:06:27 +08:00
耗子 3507517d5b feat: 优化密码显示,可点击查看 2024-10-24 22:08:30 +08:00
renovate[bot] 17cce7ec2a chore(deps): Update dependency @types/node to v20.17.0 (#293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 09:43:58 +00:00
耗子 a2b30b07bb fix: 首页mysql数据库数量不显示 2024-10-24 14:03:32 +08:00
耗子 ffb7bfda51 Merge remote-tracking branch 'origin/main' 2024-10-24 13:41:16 +08:00
耗子 d36dd8fded feat: 终端使用可变字体 2024-10-24 13:41:10 +08:00
renovate[bot] a5f69db353 chore(deps): Update dependency sass to v1.80.4 (#295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 04:10:09 +00:00
renovate[bot] 4606c63c81 chore(deps): Update dependency npm-run-all2 to v7.0.1 (#294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 00:34:58 +00:00
耗子 247000718d feat: 优化默认主题配色 2024-10-24 03:31:53 +08:00
耗子 aedde0d433 feat: 发布v2.3.13 2024-10-24 03:08:23 +08:00
耗子 0b61f2f67c fix: 容器管理多一个空元素 2024-10-24 03:04:55 +08:00
耗子 b166022cea feat: 优化监控开关样式 2024-10-24 02:51:53 +08:00
耗子 fb88060824 feat: 大部分视图支持消息总线 2024-10-24 02:24:07 +08:00
耗子 f760dbaf80 refactor: 消息总线 2024-10-24 01:59:52 +08:00
耗子 6050bf611b feat: 重构证书组件 2024-10-24 01:48:57 +08:00
耗子 0b425f09ee feat: 移除思源黑体,减小打包大小 2024-10-24 00:38:01 +08:00
耗子 f18c39ff60 feat: 缩小面板侧边栏宽度 2024-10-24 00:28:45 +08:00
耗子 9db30ac11b fix: 加回旧版的arg检查 2024-10-23 22:51:18 +08:00
耗子 3c8f606d45 feat: 添加一层root用户检查 2024-10-23 22:38:30 +08:00
renovate[bot] 59c08f0850 chore(deps): Update dependency @vue/eslint-config-prettier to v10.1.0 (#292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 21:54:38 +08:00
renovate[bot] 29a7aa4ca5 chore(deps): Update dependency @types/node to v20.16.15 (#289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 21:54:27 +08:00
renovate[bot] 8eb75a921e chore(deps): Update dependency @iconify/json to v2.2.263 (#291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 11:47:40 +00:00
耗子 86fb1c392e fix: 优化实时日志modal重载 2024-10-23 15:47:53 +08:00
耗子 df4e47cc7b fix: supervisor不支持重载 2024-10-23 15:35:20 +08:00
耗子 6101bedee3 fix: 优化网站列表获取 2024-10-23 15:26:23 +08:00
耗子 817e86466c fix: 网站列表HTTPS显示不正确 2024-10-23 15:21:52 +08:00
耗子 710601600a Merge remote-tracking branch 'origin/main' 2024-10-23 15:09:24 +08:00
耗子 63a934510e fix: 统一重定向状态码为308 2024-10-23 15:09:17 +08:00
renovate[bot] f3aac307d0 chore(deps): Update dependency vite to v5.4.10 (#290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 13:11:00 +08:00
耗子 e32c887bd7 feat: 终端固定背景颜色防止闪烁 2024-10-23 04:30:14 +08:00
耗子 bc37511b15 fix: 实时日志调整为1000行 2024-10-23 04:24:00 +08:00
耗子 72652eb814 fix: remove debug 2024-10-23 04:18:31 +08:00
耗子 6e3db107ec fix: go embed error 2024-10-23 04:06:04 +08:00
耗子 81843470b7 feat: 发布v2.3.11 2024-10-23 03:13:44 +08:00
renovate[bot] a539532ae2 chore(deps): Update unocss monorepo to v0.63.6 (#288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 03:12:40 +08:00
耗子 fc1584e7da fix: 罕见情况下防火墙规则错误 2024-10-23 03:05:40 +08:00
耗子 3c72a66c1f feat: ssh多机管理 2024-10-23 02:39:43 +08:00
renovate[bot] 346ec7ab1f chore(deps): Update dependency @types/node to v20.16.14 (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 07:07:18 +00:00
renovate[bot] 7850b74966 chore(deps): Update dependency npm-run-all2 to v7 (#286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 10:17:28 +08:00
耗子 f2d2bad52f feat: 初始化ssh模型 2024-10-22 02:59:53 +08:00
耗子 0786451c79 docs: 添加微信群 2024-10-22 02:52:56 +08:00
renovate[bot] 0291ae7637 chore(deps): Lock file maintenance (#283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 16:01:50 +08:00
renovate[bot] de76376e97 chore(deps): Update dependency md-editor-v3 to v4.21.1 (#282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 16:01:26 +08:00
耗子 de21179d61 fix: lint 2024-10-21 11:56:53 +08:00
renovate[bot] b81f3de01d chore(deps): Lock file maintenance (#281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 11:55:54 +08:00
renovate[bot] be3bae7d57 chore(deps): Update module github.com/klauspost/compress to v1.17.11 (#278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:39:07 +08:00
renovate[bot] ffb4e21f48 chore(deps): Update dependency eslint-plugin-vue to v9.29.1 (#279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:38:58 +08:00
renovate[bot] 39624d8e1c chore(deps): Update dependency @iconify/json to v2.2.262 (#280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:38:48 +08:00
耗子 1378413e54 fix: lint 2024-10-21 01:35:32 +08:00
耗子 730aea037c feat: 应用支持实时日志 2024-10-21 01:33:13 +08:00
耗子 5ae6135429 fix: 实时日志优化 2024-10-21 01:16:09 +08:00
耗子 84703391a3 feat: 支持实时日志流 2024-10-21 01:08:10 +08:00
耗子 354705ec5b fix: cron添加脚本不保存 2024-10-21 01:07:37 +08:00
耗子 f182eab2dc fix: test 2024-10-20 22:29:25 +08:00
耗子 2828795fb0 fix: test 2024-10-20 22:21:31 +08:00
耗子 e6aad21631 fix: lint 2024-10-20 22:15:23 +08:00
耗子 ff239c467a refactor: 面板websocket 2024-10-20 22:14:11 +08:00
耗子 892f91be3e feat: 开发服务器代理支持多目标 2024-10-20 19:55:14 +08:00
耗子 d51d11f460 feat: 文件远程下载 2024-10-20 19:20:39 +08:00
耗子 f9233bd36b feat: 文件搜索 2024-10-20 18:51:05 +08:00
耗子 a2ebc070ae feat: 文件管理优化 2024-10-20 17:28:02 +08:00
耗子 480a6a829d feat: 终端支持字体缩放 2024-10-20 05:14:48 +08:00
耗子 47b92a8b2a refactor: 重写ssh 2024-10-20 04:38:27 +08:00
耗子 1a7f679fca feat: 发布v2.3.10 2024-10-20 01:47:00 +08:00
耗子 8b6b94ea1f feat: 跑分插件优化 2024-10-20 01:46:25 +08:00
耗子 0d9c8b3a2d feat: 跑分插件 2024-10-20 01:34:22 +08:00
耗子 2be97a4543 feat: 优化颜色选择器 2024-10-19 21:05:25 +08:00
renovate[bot] 7b6b86f0f6 chore(deps): Update dependency @types/node to v20.16.13 (#277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 21:04:55 +08:00
耗子 6f82242d92 feat: 颜色选择器 2024-10-19 21:01:46 +08:00
耗子 7f301cec58 feat: 组件支持固定 2024-10-19 20:47:33 +08:00
耗子 c75cfe9eb6 feat: 前端路由固定 2024-10-19 20:18:23 +08:00
耗子 b94664ed87 Merge remote-tracking branch 'origin/main' 2024-10-19 18:01:26 +08:00
耗子 93fc5e08f6 feat: 前端优化 2024-10-19 18:01:18 +08:00
renovate[bot] 9bcfd4cd73 chore(deps): Update dependency sass to v1.80.3 (#276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 15:35:39 +08:00
耗子 2d10110eef fix: test 2024-10-19 15:01:44 +08:00
耗子 78e31fdc40 chore: 添加aff 2024-10-19 14:41:47 +08:00
耗子 d2b1394950 feat(HTTPS): 证书支持华为云 2024-10-19 05:14:00 +08:00
耗子 389b0a005b feat(HTTPS): 优化证书申请 2024-10-19 03:03:38 +08:00
renovate[bot] 0b1554c2c6 chore(deps): Update dependency npm-run-all2 to v6.2.4 (#274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 17:17:39 +08:00
renovate[bot] 3b580e2827 chore(deps): Update dependency sass to v1.80.2 (#273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 08:48:22 +08:00
耗子 595f5ba2d9 fix: tests 2024-10-18 02:28:08 +08:00
耗子 78543dac67 feat: 发布v2.3.9 2024-10-18 02:11:58 +08:00
耗子 e9cbc8e945 refactor: 重构防火墙 2024-10-18 02:08:55 +08:00
耗子 905e206f9c fix: test 2024-10-17 23:27:11 +08:00
耗子 b5b1992e3b fix: build 2024-10-17 23:05:38 +08:00
耗子 a7fe0c87a5 feat: 首页资源总览优化 2024-10-17 22:46:49 +08:00
耗子 5dcbac1c93 feat(工具箱): 支持设置主机名和时间 2024-10-17 22:17:20 +08:00
耗子 67a6d5cea8 feat: 添加应用图标 2024-10-17 21:22:26 +08:00
耗子 f6fec06e82 feat: 添加应用图标 2024-10-17 21:20:26 +08:00
耗子 2d74e8966e fix: 优化表格样式 2024-10-17 20:30:56 +08:00
耗子 065877547e fix: 固定按钮防止错位 2024-10-17 20:17:06 +08:00
耗子 12b6a1af35 feat: 优化首页布局 2024-10-17 18:55:08 +08:00
耗子 b36f7f3085 feat: 添加密码复杂度验证 2024-10-17 18:40:27 +08:00
耗子 2112e86329 fix: 修复报错Slug不存在 2024-10-17 18:02:48 +08:00
耗子 f2c3569447 fix: 修改部分端口报错 2024-10-17 17:54:33 +08:00
耗子 1a9fe40b80 feat: cli支持更新应用 2024-10-17 17:32:38 +08:00
250 changed files with 9523 additions and 4412 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ body:
required: false
- label: 这个问题可以被稳定复现 (The problem can be stably reproduced)
required: false
- label: 问题是在升级之后产生的 (The problem is generated after upgrading)
- label: 问题是在更新之后产生的 (The problem is generated after upgrading)
required: false
- type: dropdown
id: system
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
- name: Install dependencies
run: go mod tidy
- name: Run tests with coverage
run: go test -v -coverprofile="coverage.out" ./...
run: sudo go test -v -coverprofile="coverage.out" ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v4
with:
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
# We need to run the dev server first to generate the auto-imports files
run: |
cp .env.production .env
cp settings/proxy-config.ts.example settings/proxy-config.ts
cp settings/proxy-config.example.ts settings/proxy-config.ts
pnpm dev &
sleep 5
kill %1
+1 -1
View File
@@ -21,4 +21,4 @@ jobs:
run: |
cp config.example.yml config.yml
- name: Run tests
run: go test ./...
run: sudo go test ./...
+2 -2
View File
@@ -18,7 +18,7 @@
耗子面板是使用 Golang + Vue 开发的开源轻量 Linux 服务器运维管理面板。
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.com/a/TheTNB)
QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 微信群:[复制此链接](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.com/a/TheTNB)
## 优势
@@ -124,7 +124,7 @@ panel-cli
### 服务器
<a href="https://www.dkdun.cn/">
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
<img height="80" src=".github/assets/dk.png" alt="林枫云">
</a>
+2 -2
View File
@@ -18,7 +18,7 @@
The Rat Panel is an open source lightweight Linux server operation and maintenance management panel developed using Golang and Vue.
Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | WeChat group: [Copy this link](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
## Advantages
@@ -124,7 +124,7 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
### Server
<a href="https://www.dkdun.cn/">
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
<img height="80" src=".github/assets/dk.png" alt="林枫云">
</a>
+5 -7
View File
@@ -20,16 +20,16 @@ require (
github.com/go-resty/resty/v2 v2.15.3
github.com/go-sql-driver/mysql v1.8.1
github.com/goccy/go-yaml v1.12.0
github.com/gookit/color v1.5.4
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-version v1.7.0
github.com/klauspost/compress v1.17.11
github.com/knadh/koanf/parsers/yaml v0.1.0
github.com/knadh/koanf/providers/file v1.1.2
github.com/knadh/koanf/v2 v2.1.1
github.com/lib/pq v1.10.9
github.com/libdns/alidns v1.0.3
github.com/libdns/cloudflare v0.1.1
github.com/libdns/dnspod v0.0.3
github.com/libdns/huaweicloud v0.2.2
github.com/libdns/libdns v0.2.2
github.com/libdns/tencentcloud v1.0.0
github.com/mholt/acmez/v2 v2.0.3
@@ -60,6 +60,7 @@ require (
github.com/connesc/cipherio v0.2.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
@@ -78,12 +79,11 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jaevor/go-nanoid v1.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@@ -96,7 +96,6 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
@@ -109,7 +108,6 @@ require (
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
@@ -132,4 +130,4 @@ require (
modernc.org/sqlite v1.32.0 // indirect
)
replace github.com/mholt/acmez/v2 => github.com/TheTNB/acmez/v2 v2.0.0-20241012163130-5833d84639e0
replace github.com/mholt/acmez/v2 => github.com/TheTNB/acmez/v2 v2.0.0-20241025203320-cc718c4c870b
+46 -14
View File
@@ -23,8 +23,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/TheTNB/acmez/v2 v2.0.0-20241012163130-5833d84639e0 h1:o66ZhpjXVs7oi3VyiZvlpp9StFFe4KTeklzB/T4s59Q=
github.com/TheTNB/acmez/v2 v2.0.0-20241012163130-5833d84639e0/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
github.com/TheTNB/acmez/v2 v2.0.0-20241025203320-cc718c4c870b h1:1iVxCglpEJ/kq48YcujkWq8SeOacMuMmuekMmGyOSPA=
github.com/TheTNB/acmez/v2 v2.0.0-20241025203320-cc718c4c870b/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
@@ -50,6 +50,8 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 h1:AjCMD0AEGHHbcIpvRcrI8rMA+Us5F4uUt3B3KSpiYN8=
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7/go.mod h1:Gz/Ng6WCvYc1Q2smuPvow9kszfyH47wtnpZzvdD2juA=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
@@ -162,14 +164,13 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
@@ -189,8 +190,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -218,10 +219,9 @@ github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=
github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/libdns/dnspod v0.0.3 h1:xJHDIujgLjvZnpB8/rMoCHUqA/KxSGBqRUXxSIzNzAA=
github.com/libdns/dnspod v0.0.3/go.mod h1:XLnqMmK7QlLPEbHwcOxbRvlzRvDgaaUlthRNFOPjXPI=
github.com/libdns/huaweicloud v0.2.2 h1:DvaWLiPJ0hoOvvPafKXgWv4gG33O8cqEu87GJeXYtbc=
github.com/libdns/huaweicloud v0.2.2/go.mod h1:F9vNIca+Cq1ZJiMCSkHQUdvXEGY6UbLoaYI3Py9CFDs=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/tencentcloud v1.0.0 h1:u4LXnYu/lu/9P5W+MCVPeSDnwI+6w+DxYhQ1wSnQOuU=
@@ -247,8 +247,6 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -284,11 +282,15 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 h1:C0GHdLTfikLVoEzfhgPfrZ7LwlG0xiCmk6iwNKE+xs0=
@@ -306,11 +308,10 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v3 v3.0.0-alpha9.1 h1:1fJU+bltkwN8lF4Sni/X0i1d8XwPIrS82ivZ8qsp/q4=
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/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -351,6 +352,9 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -380,6 +384,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -398,7 +404,12 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -414,6 +425,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -436,10 +449,22 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -447,6 +472,11 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -480,6 +510,8 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+18
View File
@@ -0,0 +1,18 @@
package benchmark
import (
"github.com/go-chi/chi/v5"
"github.com/TheTNB/panel/pkg/apploader"
"github.com/TheTNB/panel/pkg/types"
)
func init() {
apploader.Register(&types.App{
Slug: "benchmark",
Route: func(r chi.Router) {
service := NewService()
r.Post("/test", service.Test)
},
})
}
+6
View File
@@ -0,0 +1,6 @@
package benchmark
type Test struct {
Name string `json:"name" validate:"required,oneof=image machine compile encryption compression physics json memory disk"`
Multi bool `json:"multi"`
}
+686
View File
@@ -0,0 +1,686 @@
package benchmark
import (
"bytes"
"crypto/aes"
"crypto/cipher"
cryptorand "crypto/rand"
"encoding/json"
"fmt"
"image"
"image/color"
"io"
"math"
"math/big"
"math/rand/v2"
"net/http"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/klauspost/compress/zstd"
"github.com/TheTNB/panel/internal/service"
)
type Service struct {
}
func NewService() *Service {
return &Service{}
}
// Test 运行测试
func (s *Service) Test(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Test](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
switch req.Name {
case "image":
result := s.imageProcessing(req.Multi)
service.Success(w, result)
case "machine":
result := s.machineLearning(req.Multi)
service.Success(w, result)
case "compile":
result := s.compileSimulationSingle(req.Multi)
service.Success(w, result)
case "encryption":
result := s.encryptionTest(req.Multi)
service.Success(w, result)
case "compression":
result := s.compressionTest(req.Multi)
service.Success(w, result)
case "physics":
result := s.physicsSimulation(req.Multi)
service.Success(w, result)
case "json":
result := s.jsonProcessing(req.Multi)
service.Success(w, result)
case "disk":
result := s.diskTestTask()
service.Success(w, result)
case "memory":
result := s.memoryTestTask()
service.Success(w, result)
default:
service.Error(w, http.StatusUnprocessableEntity, "未知测试类型")
}
}
// calculateCpuScore 计算CPU成绩
func (s *Service) calculateCpuScore(duration time.Duration) int {
score := int((10 / duration.Seconds()) * float64(3000))
if score < 0 {
score = 0
}
return score
}
// calculateScore 计算内存/硬盘成绩
func (s *Service) calculateScore(duration time.Duration) int {
score := int((20 / duration.Seconds()) * float64(30000))
if score < 0 {
score = 0
}
return score
}
// 图像处理
func (s *Service) imageProcessing(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
if err := s.imageProcessingTask(n); err != nil {
return 0
}
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) imageProcessingTask(numThreads int) error {
img := image.NewRGBA(image.Rect(0, 0, 4000, 4000))
for x := 0; x < 4000; x++ {
for y := 0; y < 4000; y++ {
img.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), A: 255})
}
}
var wg sync.WaitGroup
dx := img.Bounds().Dx()
dy := img.Bounds().Dy()
chunkSize := dy / numThreads
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
startY := i * chunkSize
endY := startY + chunkSize
if i == numThreads-1 {
endY = dy
}
for x := 1; x < dx-1; x++ {
for y := startY + 1; y < endY-1; y++ {
// 卷积操作(模糊)
rTotal, gTotal, bTotal := 0, 0, 0
for k := -1; k <= 1; k++ {
for l := -1; l <= 1; l++ {
r, g, b, _ := img.At(x+k, y+l).RGBA()
rTotal += int(r)
gTotal += int(g)
bTotal += int(b)
}
}
rAvg := uint8(rTotal / 9 / 256)
gAvg := uint8(gTotal / 9 / 256)
bAvg := uint8(bTotal / 9 / 256)
img.Set(x, y, color.RGBA{R: rAvg, G: gAvg, B: bAvg, A: 255})
}
}
}(i)
}
wg.Wait()
return nil
}
// 机器学习(矩阵乘法)
func (s *Service) machineLearning(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.machineLearningTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) machineLearningTask(numThreads int) {
size := 900
a := make([][]float64, size)
b := make([][]float64, size)
for i := 0; i < size; i++ {
a[i] = make([]float64, size)
b[i] = make([]float64, size)
for j := 0; j < size; j++ {
a[i][j] = rand.Float64()
b[i][j] = rand.Float64()
}
}
c := make([][]float64, size)
for i := 0; i < size; i++ {
c[i] = make([]float64, size)
}
var wg sync.WaitGroup
chunkSize := size / numThreads
for k := 0; k < numThreads; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
start := k * chunkSize
end := start + chunkSize
if k == numThreads-1 {
end = size
}
for i := start; i < end; i++ {
for j := 0; j < size; j++ {
sum := 0.0
for l := 0; l < size; l++ {
sum += a[i][l] * b[l][j]
}
c[i][j] = sum
}
}
}(k)
}
wg.Wait()
}
// 数学问题(计算斐波那契数)
func (s *Service) compileSimulationSingle(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
totalCalculations := 1000
fibNumber := 20000
calculationsPerThread := totalCalculations / n
remainder := totalCalculations % n
var wg sync.WaitGroup
for i := 0; i < n; i++ {
tasks := calculationsPerThread
if i < remainder {
tasks++ // 处理无法均分的剩余任务
}
wg.Add(1)
go func(tasks int) {
defer wg.Done()
for j := 0; j < tasks; j++ {
s.fib(fibNumber)
}
}(tasks)
}
wg.Wait()
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
// 斐波那契函数
func (s *Service) fib(n int) *big.Int {
if n < 2 {
return big.NewInt(int64(n))
}
a := big.NewInt(0)
b := big.NewInt(1)
temp := big.NewInt(0)
for i := 2; i <= n; i++ {
temp.Add(a, b)
a.Set(b)
b.Set(temp)
}
return b
}
// AES加密
func (s *Service) encryptionTest(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
if err := s.encryptionTestTask(n); err != nil {
return 0
}
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) encryptionTestTask(numThreads int) error {
key := []byte("abcdefghijklmnopqrstuvwxyz123456")
dataSize := 1 * 1024 * 1024 * 1024 // 1GB
plaintext := []byte(strings.Repeat("A", dataSize))
block, err := aes.NewCipher(key)
if err != nil {
return err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return err
}
chunkSize := dataSize / numThreads
var wg sync.WaitGroup
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
start := i * chunkSize
end := start + chunkSize
if i == numThreads-1 {
end = dataSize
}
nonce := make([]byte, aesGCM.NonceSize())
if _, err = cryptorand.Read(nonce); err != nil {
return
}
aesGCM.Seal(nil, nonce, plaintext[start:end], nil)
}(i)
}
wg.Wait()
return nil
}
// 压缩/解压缩
func (s *Service) compressionTest(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.compressionTestTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) compressionTestTask(numThreads int) {
data := []byte(strings.Repeat("耗子面板", 50000000))
chunkSize := len(data) / numThreads
var wg sync.WaitGroup
compressedChunks := make([]bytes.Buffer, numThreads)
// 压缩
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
start := i * chunkSize
end := start + chunkSize
if i == numThreads-1 {
end = len(data)
}
var buf bytes.Buffer
zw, _ := zstd.NewWriter(&buf)
_, _ = zw.Write(data[start:end])
_ = zw.Close()
compressedChunks[i] = buf
}(i)
}
wg.Wait()
// 解压缩
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
zr, err := zstd.NewReader(&compressedChunks[i])
if err != nil {
return
}
_, err = io.Copy(io.Discard, zr)
if err != nil {
return
}
zr.Close()
}(i)
}
wg.Wait()
}
// 物理仿真(N体问题)
func (s *Service) physicsSimulation(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.physicsSimulationTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) physicsSimulationTask(numThreads int) {
const (
numBodies = 4000
steps = 30
)
type Body struct {
x, y, z, vx, vy, vz float64
}
bodies := make([]Body, numBodies)
for i := 0; i < numBodies; i++ {
bodies[i] = Body{
x: rand.Float64(),
y: rand.Float64(),
z: rand.Float64(),
vx: rand.Float64(),
vy: rand.Float64(),
vz: rand.Float64(),
}
}
chunkSize := numBodies / numThreads
for step := 0; step < steps; step++ {
var wg sync.WaitGroup
// 更新速度
for k := 0; k < numThreads; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
start := k * chunkSize
end := start + chunkSize
if k == numThreads-1 {
end = numBodies
}
for i := start; i < end; i++ {
bi := &bodies[i]
for j := 0; j < numBodies; j++ {
if i == j {
continue
}
bj := &bodies[j]
dx := bj.x - bi.x
dy := bj.y - bi.y
dz := bj.z - bi.z
dist := math.Sqrt(dx*dx + dy*dy + dz*dz)
if dist == 0 {
continue
}
force := 1 / (dist * dist)
bi.vx += force * dx / dist
bi.vy += force * dy / dist
bi.vz += force * dz / dist
}
}
}(k)
}
wg.Wait()
// 更新位置
for k := 0; k < numThreads; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
start := k * chunkSize
end := start + chunkSize
if k == numThreads-1 {
end = numBodies
}
for i := start; i < end; i++ {
bi := &bodies[i]
bi.x += bi.vx
bi.y += bi.vy
bi.z += bi.vz
}
}(k)
}
wg.Wait()
}
}
// JSON解析
func (s *Service) jsonProcessing(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.jsonProcessingTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) jsonProcessingTask(numThreads int) {
numElements := 1000000
elementsPerThread := numElements / numThreads
var wg sync.WaitGroup
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
start := i * elementsPerThread
end := start + elementsPerThread
if i == numThreads-1 {
end = numElements
}
elements := make([]map[string]any, 0, end-start)
for j := start; j < end; j++ {
elements = append(elements, map[string]any{
"id": j,
"value": fmt.Sprintf("Value%d", j),
})
}
encoded, err := json.Marshal(elements)
if err != nil {
return
}
var parsed []map[string]any
err = json.Unmarshal(encoded, &parsed)
if err != nil {
return
}
}(i)
}
wg.Wait()
}
// 内存性能
func (s *Service) memoryTestTask() map[string]any {
results := make(map[string]any)
dataSize := 500 * 1024 * 1024 // 500 MB
data := make([]byte, dataSize)
_, _ = cryptorand.Read(data)
start := time.Now()
// 内存读写速度
results["bandwidth"] = s.memoryBandwidthTest(data)
// 内存访问延迟
data = data[:100*1024*1024] // 100 MB
results["latency"] = s.memoryLatencyTest(data)
duration := time.Since(start)
results["score"] = s.calculateScore(duration)
return results
}
func (s *Service) memoryBandwidthTest(data []byte) string {
dataSize := len(data)
startTime := time.Now()
for i := 0; i < dataSize; i++ {
data[i] ^= 0xFF
}
duration := time.Since(startTime).Seconds()
if duration == 0 {
return "N/A"
}
speed := float64(dataSize) / duration / (1024 * 1024)
return fmt.Sprintf("%.2f MB/s", speed)
}
func (s *Service) memoryLatencyTest(data []byte) string {
dataSize := len(data)
indices := rand.Perm(dataSize)
startTime := time.Now()
sum := byte(0)
for _, idx := range indices {
sum ^= data[idx]
}
duration := time.Since(startTime).Seconds()
if duration == 0 {
return "N/A"
}
avgLatency := duration * 1e9 / float64(dataSize)
return fmt.Sprintf("%.2f ns", avgLatency)
}
// 硬盘IO
func (s *Service) diskTestTask() map[string]any {
results := make(map[string]any)
blockSizes := []int64{4 * 1024, 64 * 1024, 512 * 1024, 1 * 1024 * 1024} // 4K, 64K, 512K, 1M
fileSize := int64(100 * 1024 * 1024) // 100MB 文件
start := time.Now()
for _, blockSize := range blockSizes {
result := s.diskIOTest(blockSize, fileSize)
results[fmt.Sprintf("%d", blockSize/1024)] = result
}
duration := time.Since(start)
results["score"] = s.calculateScore(duration)
return results
}
func (s *Service) diskIOTest(blockSize int64, fileSize int64) map[string]any {
result := make(map[string]any)
tempFile := fmt.Sprintf("tempfile_%d", blockSize)
defer os.Remove(tempFile)
// 写测试
writeSpeed, writeIOPS := s.diskWriteTest(tempFile, blockSize, fileSize)
// 读测试
readSpeed, readIOPS := s.diskReadTest(tempFile, blockSize, fileSize)
result["write_speed"] = fmt.Sprintf("%.2f MB/s", writeSpeed)
result["write_iops"] = fmt.Sprintf("%.2f IOPS", writeIOPS)
result["read_speed"] = fmt.Sprintf("%.2f MB/s", readSpeed)
result["read_iops"] = fmt.Sprintf("%.2f IOPS", readIOPS)
return result
}
func (s *Service) diskWriteTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
totalBlocks := fileSize / blockSize
data := make([]byte, blockSize)
_, _ = cryptorand.Read(data)
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_SYNC, 0644)
if err != nil {
return 0, 0
}
defer file.Close()
start := time.Now()
for i := int64(0); i < totalBlocks; i++ {
// 生成随机偏移
offset := rand.Int64N(fileSize - blockSize + 1)
_, err := file.WriteAt(data, offset)
if err != nil {
return 0, 0
}
}
_ = file.Sync()
duration := time.Since(start).Seconds()
if duration == 0 {
duration = 1
}
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
iops := float64(totalBlocks) / duration
return speed, iops
}
func (s *Service) diskReadTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
totalBlocks := fileSize / blockSize
data := make([]byte, blockSize)
file, err := os.OpenFile(fileName, os.O_RDONLY|os.O_SYNC, 0644)
if err != nil {
return 0, 0
}
defer file.Close()
start := time.Now()
for i := int64(0); i < totalBlocks; i++ {
// 生成随机偏移
offset := rand.Int64N(fileSize - blockSize + 1)
_, err := file.ReadAt(data, offset)
if err != nil && err != io.EOF {
return 0, 0
}
}
duration := time.Since(start).Seconds()
if duration == 0 {
duration = 1
}
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
iops := float64(totalBlocks) / duration
return speed, iops
}
+1
View File
@@ -3,6 +3,7 @@ package apps
import (
"github.com/go-chi/chi/v5"
_ "github.com/TheTNB/panel/internal/apps/benchmark"
_ "github.com/TheTNB/panel/internal/apps/fail2ban"
_ "github.com/TheTNB/panel/internal/apps/frp"
_ "github.com/TheTNB/panel/internal/apps/gitea"
+1 -1
View File
@@ -5,5 +5,5 @@ type UpdateConfig struct {
}
type SetRootPassword struct {
Password string `form:"password" json:"password" validate:"required"`
Password string `form:"password" json:"password" validate:"required,password"`
}
+2 -4
View File
@@ -144,8 +144,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
// ErrorLog 获取错误日志
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 100 %s/server/mysql/mysql-error.log", app.Root)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-error.log", app.Root))
}
// ClearErrorLog 清空错误日志
@@ -160,8 +159,7 @@ func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
// SlowLog 获取慢查询日志
func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 100 %s/server/mysql/mysql-slow.log", app.Root)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-slow.log", app.Root))
}
// ClearSlowLog 清空慢查询日志
+1 -2
View File
@@ -58,8 +58,7 @@ func (s *Service) SaveConfig(w http.ResponseWriter, r *http.Request) {
}
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
out, _ := shell.Execf("tail -n 100 %s/%s", app.Root, "wwwlogs/nginx-error.log")
service.Success(w, out)
service.Success(w, fmt.Sprintf("%s/%s", app.Root, "wwwlogs/nginx-error.log"))
}
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
+2 -4
View File
@@ -111,13 +111,11 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
}
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/php-fpm.log", app.Root, s.version)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/php-fpm.log", app.Root, s.version))
}
func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/slow.log", app.Root, s.version)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/slow.log", app.Root, s.version))
}
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
+3 -1
View File
@@ -78,9 +78,11 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
fw := firewall.NewFirewall()
err = fw.Port(firewall.FireInfo{
Type: firewall.TypeNormal,
PortStart: req.Port,
PortEnd: req.Port,
Protocol: "tcp",
Direction: firewall.DirectionIn,
Strategy: firewall.StrategyAccept,
}, firewall.OperationAdd)
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
+1 -2
View File
@@ -132,8 +132,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
// Log 获取日志
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 100 %s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly))
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly)))
}
// ClearLog 清空日志
+2 -2
View File
@@ -2,7 +2,7 @@ package pureftpd
type Create struct {
Username string `form:"username" json:"username" validate:"required"`
Password string `form:"password" json:"password" validate:"required"`
Password string `form:"password" json:"password" validate:"required,password"`
Path string `form:"path" json:"path" validate:"required"`
}
@@ -12,7 +12,7 @@ type Delete struct {
type ChangePassword struct {
Username string `form:"username" json:"username" validate:"required"`
Password string `form:"password" json:"password" validate:"required"`
Password string `form:"password" json:"password" validate:"required,password"`
}
type UpdatePort struct {
+3 -1
View File
@@ -156,9 +156,11 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
fw := firewall.NewFirewall()
err = fw.Port(firewall.FireInfo{
Type: firewall.TypeNormal,
PortStart: req.Port,
PortEnd: req.Port,
Protocol: "tcp",
Direction: firewall.DirectionIn,
Strategy: firewall.StrategyAccept,
}, firewall.OperationAdd)
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
-1
View File
@@ -13,7 +13,6 @@ func init() {
Route: func(r chi.Router) {
service := NewService()
r.Get("/service", service.Service)
r.Get("/log", service.Log)
r.Post("/clearLog", service.ClearLog)
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
+1 -8
View File
@@ -36,12 +36,6 @@ func (s *Service) Service(w http.ResponseWriter, r *http.Request) {
service.Success(w, s.name)
}
// Log 日志
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf(`tail -n 200 /var/log/supervisor/supervisord.log`)
service.Success(w, log)
}
// ClearLog 清空日志
func (s *Service) ClearLog(w http.ResponseWriter, r *http.Request) {
if _, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
@@ -204,8 +198,7 @@ func (s *Service) ProcessLog(w http.ResponseWriter, r *http.Request) {
return
}
log, _ := shell.Execf(`tail -n 200 '%s'`, logPath)
service.Success(w, log)
service.Success(w, logPath)
}
// ClearProcessLog 清空进程日志
+4
View File
@@ -18,6 +18,10 @@ func init() {
r.Post("/swap", service.UpdateSWAP)
r.Get("/timezone", service.GetTimezone)
r.Post("/timezone", service.UpdateTimezone)
r.Post("/time", service.UpdateTime)
r.Post("/syncTime", service.SyncTime)
r.Get("/hostname", service.GetHostname)
r.Post("/hostname", service.UpdateHostname)
r.Get("/hosts", service.GetHosts)
r.Post("/hosts", service.UpdateHosts)
r.Post("/rootPassword", service.UpdateRootPassword)
+11 -1
View File
@@ -1,5 +1,7 @@
package toolbox
import "time"
type DNS struct {
DNS1 string `form:"dns1" json:"dns1" validate:"required"`
DNS2 string `form:"dns2" json:"dns2" validate:"required"`
@@ -13,10 +15,18 @@ type Timezone struct {
Timezone string `form:"timezone" json:"timezone" validate:"required"`
}
type Time struct {
Time time.Time `form:"time" json:"time" validate:"required"`
}
type Hostname struct {
Hostname string `form:"hostname" json:"hostname" validate:"required,regex=^[a-zA-Z0-9][a-zA-Z0-9-]{061}[a-zA-Z0-9]$"`
}
type Hosts struct {
Hosts string `form:"hosts" json:"hosts"`
}
type Password struct {
Password string `form:"password" json:"password" validate:"required"`
Password string `form:"password" json:"password" validate:"required,password"`
}
+59 -5
View File
@@ -12,6 +12,7 @@ import (
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/service"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/ntp"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/types"
@@ -221,6 +222,64 @@ func (s *Service) UpdateTimezone(w http.ResponseWriter, r *http.Request) {
service.Success(w, nil)
}
// UpdateTime 设置时间
func (s *Service) UpdateTime(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Time](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = ntp.UpdateSystemTime(req.Time); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
// SyncTime 同步时间
func (s *Service) SyncTime(w http.ResponseWriter, r *http.Request) {
now, err := ntp.Now()
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = ntp.UpdateSystemTime(now); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
// GetHostname 获取主机名
func (s *Service) GetHostname(w http.ResponseWriter, r *http.Request) {
hostname, _ := io.Read("/etc/hostname")
service.Success(w, strings.TrimSpace(hostname))
}
// UpdateHostname 设置主机名
func (s *Service) UpdateHostname(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Hostname](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf("hostnamectl set-hostname '%s'", req.Hostname); err != nil {
// 直接写 /etc/hostname
if err = io.Write("/etc/hostname", req.Hostname, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入主机名失败")
return
}
}
service.Success(w, nil)
}
// GetHosts 获取 hosts 信息
func (s *Service) GetHosts(w http.ResponseWriter, r *http.Request) {
hosts, _ := io.Read("/etc/hosts")
@@ -251,11 +310,6 @@ func (s *Service) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
return
}
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(req.Password) {
service.Error(w, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符")
return
}
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
if _, err = shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
+2 -1
View File
@@ -12,7 +12,7 @@ type Cert struct {
AccountID uint `gorm:"not null" json:"account_id"` // 关联的 ACME 账户 ID
WebsiteID uint `gorm:"not null" json:"website_id"` // 关联的网站 ID
DNSID uint `gorm:"not null" json:"dns_id"` // 关联的 DNS ID
Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 4096)
Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
Domains []string `gorm:"not null;serializer:json" json:"domains"`
AutoRenew bool `gorm:"not null" json:"auto_renew"` // 自动续签
CertURL string `gorm:"not null" json:"cert_url"` // 证书 URL (续签时使用)
@@ -29,6 +29,7 @@ type Cert struct {
type CertRepo interface {
List(page, limit uint) ([]*Cert, int64, error)
Get(id uint) (*Cert, error)
GetByWebsite(WebsiteID uint) (*Cert, error)
Create(req *request.CertCreate) (*Cert, error)
Update(req *request.CertUpdate) error
Delete(id uint) error
+2 -1
View File
@@ -13,7 +13,7 @@ type CertAccount struct {
Kid string `gorm:"not null" json:"kid"`
HmacEncoded string `gorm:"not null" json:"hmac_encoded"`
PrivateKey string `gorm:"not null" json:"private_key"`
KeyType string `gorm:"not null" json:"key_type"`
KeyType string `gorm:"not null" json:"key_type"` // 密钥类型 (P256, P384, 2048, 3072, 4096)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -22,6 +22,7 @@ type CertAccount struct {
type CertAccountRepo interface {
List(page, limit uint) ([]*CertAccount, int64, error)
GetDefault(userID uint) (*CertAccount, error)
Get(id uint) (*CertAccount, error)
Create(req *request.CertAccountCreate) (*CertAccount, error)
Update(req *request.CertAccountUpdate) error
-1
View File
@@ -26,5 +26,4 @@ type CronRepo interface {
Update(req *request.CronUpdate) error
Delete(id uint) error
Status(id uint, status bool) error
Log(id uint) (string, error)
}
-4
View File
@@ -17,10 +17,6 @@ const (
SettingKeyBackupPath SettingKey = "backup_path"
SettingKeyWebsitePath SettingKey = "website_path"
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
SettingKeySshHost SettingKey = "ssh_host"
SettingKeySshPort SettingKey = "ssh_port"
SettingKeySshUser SettingKey = "ssh_user"
SettingKeySshPassword SettingKey = "ssh_password"
SettingKeyOfflineMode SettingKey = "offline_mode"
)
+22 -3
View File
@@ -1,8 +1,27 @@
package biz
import "github.com/TheTNB/panel/internal/http/request"
import (
"time"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/ssh"
)
type SSH struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Host string `gorm:"not null" json:"host"`
Port uint `gorm:"not null" json:"port"`
Config ssh.ClientConfig `gorm:"not null;serializer:json" json:"config"`
Remark string `gorm:"not null" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type SSHRepo interface {
GetInfo() (map[string]any, error)
UpdateInfo(req *request.SSHUpdateInfo) error
List(page, limit uint) ([]*SSH, int64, error)
Get(id uint) (*SSH, error)
Create(req *request.SSHCreate) error
Update(req *request.SSHUpdate) error
Delete(id uint) error
}
+2
View File
@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/TheTNB/panel/internal/http/request"
@@ -33,4 +34,5 @@ type WebsiteRepo interface {
UpdateRemark(id uint, remark string) error
ResetConfig(id uint) error
UpdateStatus(id uint, status bool) error
ObtainCert(ctx context.Context, id uint) error
}
+5
View File
@@ -1,10 +1,15 @@
package bootstrap
import (
"os"
"runtime/debug"
)
func boot() {
if os.Geteuid() != 0 {
panic("panel must run as root")
}
debug.SetGCPercent(10)
debug.SetMemoryLimit(64 << 20)
+1 -2
View File
@@ -6,7 +6,6 @@ import (
"os"
"strings"
"github.com/gookit/color"
"github.com/urfave/cli/v3"
"github.com/TheTNB/panel/internal/app"
@@ -46,6 +45,6 @@ func initCli() {
Commands: route.Cli(),
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
color.Redln(fmt.Sprintf("|-%v", err))
fmt.Printf("|-%v\n", err)
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ func initConf() {
func initGlobal() {
app.Root = app.Conf.MustString("app.root")
app.Version = "2.3.8"
app.Version = "2.3.14"
app.Locale = app.Conf.MustString("app.locale")
// 初始化时区
+1
View File
@@ -23,6 +23,7 @@ func initHttp() {
// add route
route.Http(app.Http)
route.Ws(app.Http)
apps.Boot(app.Http)
srv := &http.Server{
+1 -1
View File
@@ -295,7 +295,7 @@ func (r *appRepo) Update(slug string) error {
}
task := new(biz.Task)
task.Name = "升级应用 " + item.Name
task.Name = "更新应用 " + item.Name
task.Status = biz.TaskStatusWaiting
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
task.Log = "/tmp/" + item.Slug + ".log"
+19 -20
View File
@@ -9,7 +9,6 @@ import (
"strings"
"time"
"github.com/gookit/color"
"github.com/shirou/gopsutil/disk"
"github.com/TheTNB/panel/internal/app"
@@ -182,11 +181,11 @@ func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
for _, file := range toDelete {
filePath := filepath.Join(path, file.Name())
if app.IsCli {
color.Greenln(fmt.Sprintf("|-清理过期文件:%s", filePath))
fmt.Printf("|-清理过期文件:%s\n", filePath)
}
if err = os.Remove(filePath); err != nil {
if app.IsCli {
color.Redln(fmt.Sprintf("|-清理失败:%v", err))
fmt.Printf("|-清理失败:%v\n", err)
} else {
return fmt.Errorf("清理失败:%v", err)
}
@@ -243,8 +242,8 @@ func (r *backupRepo) createWebsite(to string, name string) error {
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup))
}
return nil
}
@@ -255,7 +254,7 @@ func (r *backupRepo) createMySQL(to string, name string) error {
if err != nil {
return err
}
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
if err != nil {
return err
}
@@ -290,8 +289,8 @@ func (r *backupRepo) createMySQL(to string, name string) error {
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup+".zip"))
}
return nil
}
@@ -327,8 +326,8 @@ func (r *backupRepo) createPostgres(to string, name string) error {
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup+".zip"))
}
return nil
}
@@ -354,8 +353,8 @@ func (r *backupRepo) createPanel(to string) error {
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup))
}
return nil
}
@@ -401,7 +400,7 @@ func (r *backupRepo) restoreMySQL(backup, target string) error {
if err != nil {
return err
}
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
if err != nil {
return err
}
@@ -478,10 +477,10 @@ func (r *backupRepo) preCheckPath(to, path string) error {
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
color.Greenln(fmt.Sprintf("|-备份目录可用Inode%d", usage.InodesFree))
fmt.Printf("|-目标大小:%s\n", str.FormatBytes(float64(size)))
fmt.Printf("|-目标文件数:%d\n", files)
fmt.Printf("|-备份目录可用空间:%s\n", str.FormatBytes(float64(usage.Free)))
fmt.Printf("|-备份目录可用Inode%d\n", usage.InodesFree)
}
if uint64(size) > usage.Free {
@@ -504,9 +503,9 @@ func (r *backupRepo) preCheckDB(to string, size int64) error {
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
color.Greenln(fmt.Sprintf("|-备份目录可用Inode%d", usage.InodesFree))
fmt.Printf("|-目标大小:%s\n", str.FormatBytes(float64(size)))
fmt.Printf("|-备份目录可用空间:%s\n", str.FormatBytes(float64(usage.Free)))
fmt.Printf("|-备份目录可用Inode%d\n", usage.InodesFree)
}
if uint64(size) > usage.Free {
+15 -8
View File
@@ -17,14 +17,11 @@ import (
)
type certRepo struct {
client *acme.Client
websiteRepo biz.WebsiteRepo
client *acme.Client
}
func NewCertRepo() biz.CertRepo {
return &certRepo{
websiteRepo: NewWebsiteRepo(),
}
return &certRepo{}
}
func (r *certRepo) List(page, limit uint) ([]*biz.Cert, int64, error) {
@@ -40,6 +37,12 @@ func (r *certRepo) Get(id uint) (*biz.Cert, error) {
return cert, err
}
func (r *certRepo) GetByWebsite(WebsiteID uint) (*biz.Cert, error) {
cert := new(biz.Cert)
err := app.Orm.Model(&biz.Cert{}).Preload("Website").Preload("Account").Preload("DNS").Where("website_id = ?", WebsiteID).First(cert).Error
return cert, err
}
func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
cert := &biz.Cert{
AccountID: req.AccountID,
@@ -93,7 +96,7 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
}
}
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
client.UseHTTP(conf)
}
}
@@ -172,7 +175,7 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
}
}
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
client.UseHTTP(conf)
}
}
@@ -231,7 +234,7 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
return errors.New("该证书没有签发成功,无法部署")
}
website, err := r.websiteRepo.Get(WebsiteID)
website, err := NewWebsiteRepo().Get(WebsiteID)
if err != nil {
return err
}
@@ -251,6 +254,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
}
func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) {
if cert.Account == nil {
return nil, errors.New("该证书没有关联账号,无法签发")
}
var ca string
var eab *acme.EAB
switch cert.Account.CA {
+31 -11
View File
@@ -28,6 +28,26 @@ func (r certAccountRepo) List(page, limit uint) ([]*biz.CertAccount, int64, erro
return accounts, total, err
}
func (r certAccountRepo) GetDefault(userID uint) (*biz.CertAccount, error) {
user, err := NewUserRepo().Get(userID)
if err != nil {
return nil, err
}
account := new(biz.CertAccount)
if err = app.Orm.Model(&biz.CertAccount{}).Where("ca = ?", "googlecn").Where("email = ?", user.Email).First(account).Error; err == nil {
return account, nil
}
req := &request.CertAccountCreate{
CA: "googlecn",
Email: user.Email,
KeyType: string(acme.KeyEC256),
}
return r.Create(req)
}
func (r certAccountRepo) Get(id uint) (*biz.CertAccount, error) {
account := new(biz.CertAccount)
err := app.Orm.Model(&biz.CertAccount{}).Where("id = ?", id).First(account).Error
@@ -70,16 +90,16 @@ func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccoun
case "sslcom":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
default:
return nil, errors.New("CA 提供商不支持")
return nil, errors.New("unsupported CA")
}
if err != nil {
return nil, fmt.Errorf("注册账号失败:%v", err)
return nil, fmt.Errorf("failed to register account: %v", err)
}
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
if err != nil {
return nil, errors.New("获取私钥失败")
return nil, errors.New("failed to get private key")
}
account.PrivateKey = string(privateKey)
@@ -129,16 +149,16 @@ func (r certAccountRepo) Update(req *request.CertAccountUpdate) error {
case "sslcom":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
default:
return errors.New("CA 提供商不支持")
return errors.New("unsupported CA")
}
if err != nil {
return errors.New("向 CA 注册账号失败,请检查参数是否正确")
return errors.New("failed to register account")
}
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
if err != nil {
return errors.New("获取私钥失败")
return errors.New("failed to get private key")
}
account.PrivateKey = string(privateKey)
@@ -162,13 +182,13 @@ func (r certAccountRepo) getGoogleEAB() (*acme.EAB, error) {
client.SetTimeout(5 * time.Second)
client.SetRetryCount(2)
resp, err := client.R().SetResult(&data{}).Get("https://panel.haozi.net/api/acme/googleEAB")
resp, err := client.R().SetResult(&data{}).Get("https://gts.rat.dev/eab")
if err != nil || !resp.IsSuccess() {
return &acme.EAB{}, errors.New("获取Google EAB失败")
return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %v", err)
}
eab := resp.Result().(*data)
if eab.Message != "success" {
return &acme.EAB{}, errors.New("获取Google EAB失败")
return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %s", eab.Message)
}
return &acme.EAB{KeyID: eab.Data.KeyId, MACKey: eab.Data.MacKey}, nil
@@ -189,11 +209,11 @@ func (r certAccountRepo) getZeroSSLEAB(email string) (*acme.EAB, error) {
"email": email,
}).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email")
if err != nil || !resp.IsSuccess() {
return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败")
return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB: %v", err)
}
eab := resp.Result().(*data)
if !eab.Success {
return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败")
return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB")
}
return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil
+3 -27
View File
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"path/filepath"
"regexp"
"strconv"
"time"
@@ -54,10 +53,6 @@ func (r *cronRepo) Get(id uint) (*biz.Cron, error) {
}
func (r *cronRepo) Create(req *request.CronCreate) error {
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
return errors.New("时间格式错误")
}
var script string
if req.Type == "backup" {
if req.BackupType == "website" {
@@ -92,6 +87,9 @@ panel-cli cutoff website -n %s -p %s
panel-cli cutoff clear -t website -f %s -s %d -p %s
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
}
if req.Type == "shell" {
script = req.Script
}
shellDir := fmt.Sprintf("%s/server/cron/", app.Root)
shellLogDir := fmt.Sprintf("%s/server/cron/logs/", app.Root)
@@ -133,10 +131,6 @@ func (r *cronRepo) Update(req *request.CronUpdate) error {
return err
}
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
return errors.New("时间格式错误")
}
if !cron.Status {
return errors.New("计划任务已禁用")
}
@@ -200,24 +194,6 @@ func (r *cronRepo) Status(id uint, status bool) error {
return app.Orm.Save(cron).Error
}
func (r *cronRepo) Log(id uint) (string, error) {
cron, err := r.Get(id)
if err != nil {
return "", err
}
if !io.Exists(cron.Log) {
return "", errors.New("日志文件不存在")
}
log, err := shell.Execf("tail -n 1000 '%s'", cron.Log)
if err != nil {
return "", err
}
return log, nil
}
// addToSystem 添加到系统
func (r *cronRepo) addToSystem(cron *biz.Cron) error {
if _, err := shell.Execf(`( crontab -l; echo "%s %s >> %s 2>&1" ) | sort - | uniq - | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil {
+13 -5
View File
@@ -1,12 +1,13 @@
package data
import (
"errors"
"fmt"
"strings"
"github.com/spf13/cast"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/pkg/firewall"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
@@ -60,9 +61,9 @@ func (r *safeRepo) UpdateSSH(port uint, status bool) error {
}
func (r *safeRepo) GetPingStatus() (bool, error) {
out, err := shell.Execf(`firewall-cmd --list-all`)
if err != nil {
return true, errors.New(out)
out, err := shell.Execf(`firewall-cmd --list-rich-rules`)
if err != nil { // 可能防火墙已关闭等
return true, nil
}
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
@@ -73,7 +74,14 @@ func (r *safeRepo) GetPingStatus() (bool, error) {
}
func (r *safeRepo) UpdatePingStatus(status bool) error {
var err error
fw, err := firewall.NewFirewall().Status()
if err != nil {
return err
}
if !fw {
return fmt.Errorf("failed to update ping status: firewalld is not running")
}
if status {
_, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
} else {
+30 -29
View File
@@ -9,7 +9,6 @@ import (
"github.com/go-rat/utils/hash"
"github.com/goccy/go-yaml"
"github.com/gookit/color"
"github.com/spf13/cast"
"gorm.io/gorm"
@@ -205,9 +204,11 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
// 放行端口
fw := firewall.NewFirewall()
err = fw.Port(firewall.FireInfo{
Type: firewall.TypeNormal,
PortStart: uint(config.HTTP.Port),
PortEnd: uint(config.HTTP.Port),
Protocol: "tcp",
Direction: firewall.DirectionIn,
Strategy: firewall.StrategyAccept,
}, firewall.OperationAdd)
if err != nil {
return false, err
@@ -241,13 +242,13 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
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))
fmt.Printf("|-目标版本:%s\n", version)
fmt.Printf("|-下载链接:%s\n", url)
fmt.Printf("|-文件名:%s\n", name)
}
if app.IsCli {
color.Greenln("|-正在下载...")
fmt.Println("|-正在下载...")
}
if _, err := shell.Execf("wget -T 120 -t 3 -O /tmp/%s %s", name, url); err != nil {
return fmt.Errorf("下载失败:%w", err)
@@ -260,39 +261,39 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
}
if app.IsCli {
color.Greenln("|-校验下载文件...")
fmt.Println("|-校验下载文件...")
}
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)
fmt.Println("|-清理校验文件失败:", err)
}
return fmt.Errorf("清理校验文件失败:%w", err)
}
if app.IsCli {
color.Greenln("|-前置检查...")
fmt.Println("|-前置检查...")
}
if io.Exists("/tmp/panel-storage.zip") {
return errors.New("检测到 /tmp 存在临时文件,可能是上次升级失败所致,请运行 panel-cli fix 修复后重试")
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败所致,请运行 panel-cli fix 修复后重试")
}
if app.IsCli {
color.Greenln("|-备份面板数据...")
fmt.Println("|-备份面板数据...")
}
// 备份面板
backup := NewBackupRepo()
if err := backup.Create(biz.BackupTypePanel, ""); err != nil {
if app.IsCli {
color.Redln("|-备份面板失败:", err)
fmt.Println("|-备份面板失败:", 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 app.IsCli {
color.Redln("|-备份面板数据失败:", err)
fmt.Println("|-备份面板数据失败:", err)
}
return fmt.Errorf("备份面板数据失败:%w", err)
}
@@ -301,14 +302,14 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
}
if app.IsCli {
color.Greenln("|-清理旧版本...")
fmt.Println("|-清理旧版本...")
}
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
return fmt.Errorf("清理旧版本失败:%w", err)
}
if app.IsCli {
color.Greenln("|-解压新版本...")
fmt.Println("|-解压新版本...")
}
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel"), io.Zip); err != nil {
return fmt.Errorf("解压失败:%w", err)
@@ -318,7 +319,7 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
}
if app.IsCli {
color.Greenln("|-恢复面板数据...")
fmt.Println("|-恢复面板数据...")
}
if err := io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
return fmt.Errorf("恢复面板数据失败:%w", err)
@@ -328,10 +329,10 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
}
if app.IsCli {
color.Greenln("|-运行升级后脚本...")
fmt.Println("|-运行更新后脚本...")
}
if _, err := shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
return fmt.Errorf("运行面板升级后脚本失败:%w", err)
return fmt.Errorf("运行面板更新后脚本失败:%w", err)
}
if _, err := shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
return fmt.Errorf("下载面板服务文件失败:%w", err)
@@ -344,14 +345,14 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
}
if app.IsCli {
color.Greenln("|-设置关键文件权限...")
fmt.Println("|-设置关键文件权限...")
}
_ = io.Chmod("/usr/local/sbin/panel-cli", 0700)
_ = io.Chmod("/etc/systemd/system/panel.service", 0700)
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
if app.IsCli {
color.Greenln("|-升级完成")
fmt.Println("|-更新完成")
}
_, _ = shell.Execf("systemctl daemon-reload")
@@ -364,7 +365,7 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
func (r *settingRepo) FixPanel() error {
if app.IsCli {
color.Greenln("|-开始修复面板...")
fmt.Println("|-开始修复面板...")
}
// 检查关键文件是否正常
flag := false
@@ -378,7 +379,7 @@ func (r *settingRepo) FixPanel() error {
flag = true
}
if !flag {
return fmt.Errorf("文件正常无需修复,请运行 panel-cli update 升级面板")
return fmt.Errorf("文件正常无需修复,请运行 panel-cli update 更新面板")
}
// 再次确认是否需要修复
@@ -391,7 +392,7 @@ func (r *settingRepo) FixPanel() error {
return fmt.Errorf("清理临时文件失败:%w", err)
}
if app.IsCli {
color.Greenln("已清理临时文件,请运行 panel-cli update 升级面板")
fmt.Println("已清理临时文件,请运行 panel-cli update 更新面板")
}
return nil
}
@@ -411,12 +412,12 @@ func (r *settingRepo) FixPanel() error {
}
latest := list[0]
if app.IsCli {
color.Greenln(fmt.Sprintf("|-使用备份文件:%s", latest.Name))
fmt.Printf("|-使用备份文件:%s\n", latest.Name)
}
// 解压备份文件
if app.IsCli {
color.Greenln("|-解压备份文件...")
fmt.Println("|-解压备份文件...")
}
if err = io.Remove("/tmp/panel-fix"); err != nil {
return fmt.Errorf("清理临时目录失败:%w", err)
@@ -427,7 +428,7 @@ func (r *settingRepo) FixPanel() error {
// 移动文件到对应位置
if app.IsCli {
color.Greenln("|-移动备份文件...")
fmt.Println("|-移动备份文件...")
}
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 {
@@ -450,7 +451,7 @@ func (r *settingRepo) FixPanel() error {
// tmp目录下如果有storage备份,则解压回去
if app.IsCli {
color.Greenln("|-恢复面板数据...")
fmt.Println("|-恢复面板数据...")
}
if io.Exists("/tmp/panel-storage.zip") {
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
@@ -470,7 +471,7 @@ func (r *settingRepo) FixPanel() error {
// 处理权限
if app.IsCli {
color.Greenln("|-设置关键文件权限...")
fmt.Println("|-设置关键文件权限...")
}
if err = io.Chmod("/usr/local/etc/panel/config.yml", 0600); err != nil {
return err
@@ -486,7 +487,7 @@ func (r *settingRepo) FixPanel() error {
}
if app.IsCli {
color.Greenln("|-修复完成")
fmt.Println("|-修复完成")
}
tools.RestartPanel()
+66 -31
View File
@@ -1,12 +1,12 @@
package data
import (
"errors"
"github.com/spf13/cast"
"fmt"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
pkgssh "github.com/TheTNB/panel/pkg/ssh"
)
type sshRepo struct {
@@ -19,36 +19,71 @@ func NewSSHRepo() biz.SSHRepo {
}
}
func (r *sshRepo) GetInfo() (map[string]any, error) {
host, _ := r.settingRepo.Get(biz.SettingKeySshHost)
port, _ := r.settingRepo.Get(biz.SettingKeySshPort)
user, _ := r.settingRepo.Get(biz.SettingKeySshUser)
password, _ := r.settingRepo.Get(biz.SettingKeySshPassword)
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
return nil, errors.New("SSH 配置不完整")
}
return map[string]any{
"host": host,
"port": cast.ToInt(port),
"user": user,
"password": password,
}, nil
func (r *sshRepo) List(page, limit uint) ([]*biz.SSH, int64, error) {
var ssh []*biz.SSH
var total int64
err := app.Orm.Model(&biz.SSH{}).Omit("Hosts").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&ssh).Error
return ssh, total, err
}
func (r *sshRepo) UpdateInfo(req *request.SSHUpdateInfo) error {
if err := r.settingRepo.Set(biz.SettingKeySshHost, req.Host); err != nil {
return err
}
if err := r.settingRepo.Set(biz.SettingKeySshPort, cast.ToString(req.Port)); err != nil {
return err
}
if err := r.settingRepo.Set(biz.SettingKeySshUser, req.User); err != nil {
return err
}
if err := r.settingRepo.Set(biz.SettingKeySshPassword, req.Password); err != nil {
return err
func (r *sshRepo) Get(id uint) (*biz.SSH, error) {
ssh := new(biz.SSH)
if err := app.Orm.Where("id = ?", id).First(ssh).Error; err != nil {
return nil, err
}
return nil
return ssh, nil
}
func (r *sshRepo) Create(req *request.SSHCreate) error {
conf := pkgssh.ClientConfig{
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
User: req.User,
Password: req.Password,
Key: req.Key,
}
_, err := pkgssh.NewSSHClient(conf)
if err != nil {
return fmt.Errorf("failed to check ssh connection: %v", err)
}
ssh := &biz.SSH{
Name: req.Name,
Host: req.Host,
Port: req.Port,
Config: conf,
Remark: req.Remark,
}
return app.Orm.Create(ssh).Error
}
func (r *sshRepo) Update(req *request.SSHUpdate) error {
conf := pkgssh.ClientConfig{
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
User: req.User,
Password: req.Password,
Key: req.Key,
}
_, err := pkgssh.NewSSHClient(conf)
if err != nil {
return fmt.Errorf("failed to check ssh connection: %v", err)
}
ssh := &biz.SSH{
ID: req.ID,
Name: req.Name,
Host: req.Host,
Port: req.Port,
Config: conf,
Remark: req.Remark,
}
return app.Orm.Model(ssh).Updates(ssh).Error
}
func (r *sshRepo) Delete(id uint) error {
return app.Orm.Delete(&biz.SSH{}, id).Error
}
+61 -15
View File
@@ -1,17 +1,21 @@
package data
import (
"context"
"errors"
"fmt"
"path/filepath"
"slices"
"strings"
"github.com/samber/lo"
"github.com/spf13/cast"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/embed"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/acme"
"github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/db"
"github.com/TheTNB/panel/pkg/io"
@@ -22,14 +26,10 @@ import (
"github.com/TheTNB/panel/pkg/types"
)
type websiteRepo struct {
settingRepo biz.SettingRepo
}
type websiteRepo struct{}
func NewWebsiteRepo() biz.WebsiteRepo {
return &websiteRepo{
settingRepo: NewSettingRepo(),
}
return &websiteRepo{}
}
func (r *websiteRepo) UpdateDefaultConfig(req *request.WebsiteDefaultConfig) error {
@@ -147,8 +147,7 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
rewrite, _ := io.Read(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
setting.Rewrite = rewrite
// 访问日志
log, _ := shell.Execf(`tail -n 100 '%s/wwwlogs/%s.log'`, app.Root, website.Name)
setting.Log = log
setting.Log = fmt.Sprintf("%s/wwwlogs/%s.log", app.Root, website.Name)
return setting, err
}
@@ -289,7 +288,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
}
// 创建数据库
rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
rootPassword, err := NewSettingRepo().Get(biz.SettingKeyMySQLRootPassword)
if err == nil && req.DB && req.DBType == "mysql" {
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
if err != nil {
@@ -351,6 +350,7 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
}
// 监听地址
var listens [][]string
quic := false
for _, listen := range req.Listens {
if !listen.HTTPS && !listen.QUIC {
listens = append(listens, []string{listen.Address})
@@ -359,6 +359,7 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
listens = append(listens, []string{listen.Address, "ssl"})
}
if listen.QUIC {
quic = true
listens = append(listens, []string{listen.Address, "quic"})
}
}
@@ -416,7 +417,11 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
if err = p.SetOCSP(req.OCSP); err != nil {
return err
}
if quic {
if err = p.SetAltSvc(`'h3=":$server_port"; ma=2592000'`); err != nil {
return err
}
}
} else {
if err = p.ClearSetHTTPS(); err != nil {
return err
@@ -430,6 +435,9 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
if err = p.SetOCSP(false); err != nil {
return err
}
if err = p.SetAltSvc(``); err != nil {
return err
}
}
// 防跨站
if !strings.HasSuffix(req.Root, "/") {
@@ -484,21 +492,22 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
_ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/cert", website.Name+".key"))
_ = io.Remove(filepath.Join(app.Root, "wwwlogs", website.Name+".log"))
_ = io.Remove(filepath.Join(app.Root, "wwwlogs", website.Name+".error.log"))
if req.Path {
_ = io.Remove(website.Path)
}
if req.DB {
rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
rootPassword, err := NewSettingRepo().Get(biz.SettingKeyMySQLRootPassword)
if err != nil {
return err
}
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
if err != nil {
return err
if err == nil {
_ = mysql.DatabaseDrop(website.Name)
_ = mysql.UserDrop(website.Name)
}
_ = mysql.DatabaseDrop(website.Name)
_ = mysql.UserDrop(website.Name)
_, _ = shell.Execf(`echo "DROP DATABASE IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name)
_, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name)
}
@@ -670,3 +679,40 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error {
return nil
}
func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
website, err := r.Get(id)
if err != nil {
return err
}
if slices.Contains(website.Domains, "*") {
return errors.New("cannot one-key obtain wildcard certificate")
}
account, err := NewCertAccountRepo().GetDefault(cast.ToUint(ctx.Value("user_id")))
if err != nil {
return err
}
cRepo := NewCertRepo()
newCert, err := cRepo.GetByWebsite(website.ID)
if err != nil {
newCert, err = cRepo.Create(&request.CertCreate{
Type: string(acme.KeyEC256),
Domains: website.Domains,
AutoRenew: true,
AccountID: account.ID,
WebsiteID: website.ID,
})
if err != nil {
return err
}
}
_, err = cRepo.ObtainAuto(newCert.ID)
if err != nil {
return err
}
return cRepo.Deploy(newCert.ID, website.ID)
}
+2 -2
View File
@@ -2,8 +2,8 @@ package embed
import "embed"
//go:embed frontend/*
//go:embed all:frontend/*
var PublicFS embed.FS
//go:embed website/*
//go:embed all:website/*
var WebsiteFS embed.FS
+1 -1
View File
@@ -16,7 +16,7 @@ func Status(next http.Handler) http.Handler {
render := chix.NewRender(w)
render.Status(http.StatusServiceUnavailable)
render.JSON(chix.M{
"message": "面板升级中,请稍后刷新",
"message": "面板更新中,请稍后刷新",
})
return
case app.StatusMaintain:
+1 -1
View File
@@ -6,7 +6,7 @@ type App struct {
}
type AppSlug struct {
Slug string `json:"slug" form:"slug" validate:"required,exists=apps slug"`
Slug string `json:"slug" form:"slug" validate:"required"`
}
type AppUpdateShow struct {
+2 -2
View File
@@ -1,7 +1,7 @@
package request
type CertCreate struct {
Type string `form:"type" json:"type" validate:"required"`
Type string `form:"type" json:"type" validate:"required,oneof=P256 P384 2048 3072 4096"`
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
AccountID uint `form:"account_id" json:"account_id"`
@@ -11,7 +11,7 @@ type CertCreate struct {
type CertUpdate struct {
ID uint `form:"id" json:"id" validate:"required,exists=certs id"`
Type string `form:"type" json:"type" validate:"required"`
Type string `form:"type" json:"type" validate:"required,oneof=P256 P384 2048 3072 4096"`
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
AccountID uint `form:"account_id" json:"account_id"`
+4 -4
View File
@@ -1,18 +1,18 @@
package request
type CertAccountCreate struct {
CA string `form:"ca" json:"ca" validate:"required"`
CA string `form:"ca" json:"ca" validate:"required,oneof=googlecn google letsencrypt buypass zerossl sslcom"`
Email string `form:"email" json:"email" validate:"required"`
Kid string `form:"kid" json:"kid"`
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
KeyType string `form:"key_type" json:"key_type" validate:"required"`
KeyType string `form:"key_type" json:"key_type" validate:"required,oneof=P256 P384 2048 3072 4096"`
}
type CertAccountUpdate struct {
ID uint `form:"id" json:"id" validate:"required,exists=cert_accounts id"`
CA string `form:"ca" json:"ca" validate:"required"`
CA string `form:"ca" json:"ca" validate:"required,oneof=googlecn google letsencrypt buypass zerossl sslcom"`
Email string `form:"email" json:"email" validate:"required"`
Kid string `form:"kid" json:"kid"`
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
KeyType string `form:"key_type" json:"key_type" validate:"required"`
KeyType string `form:"key_type" json:"key_type" validate:"required,oneof=P256 P384 2048 3072 4096"`
}
+24 -2
View File
@@ -1,5 +1,16 @@
package request
import (
"net/http"
"github.com/spf13/cast"
)
type FileList struct {
Path string `json:"path" form:"path" validate:"required"`
Sort string `json:"sort" form:"sort"`
}
type FilePath struct {
Path string `json:"path" form:"path" validate:"required"`
}
@@ -11,7 +22,7 @@ type FileCreate struct {
type FileSave struct {
Path string `form:"path" json:"path" validate:"required"`
Content string `form:"content" json:"content" validate:"required"`
Content string `form:"content" json:"content"`
}
type FileMove struct {
@@ -26,6 +37,11 @@ type FileCopy struct {
Force bool `form:"force" json:"force"`
}
type FileRemoteDownload struct {
Path string `form:"path" json:"path" validate:"required"`
URL string `form:"url" json:"url" validate:"required"`
}
type FilePermission struct {
Path string `form:"path" json:"path" validate:"required"`
Mode string `form:"mode" json:"mode" validate:"required"`
@@ -45,5 +61,11 @@ type FileUnCompress struct {
type FileSearch struct {
Path string `form:"path" json:"path" validate:"required"`
KeyWord string `form:"keyword" json:"keyword" validate:"required"`
Keyword string `form:"keyword" json:"keyword" validate:"required"`
Sub bool `form:"sub" json:"sub"`
}
func (r *FileSearch) Prepare(req *http.Request) error {
r.Sub = cast.ToBool(req.FormValue("sub"))
return nil
}
+16
View File
@@ -5,6 +5,7 @@ type FirewallStatus struct {
}
type FirewallRule struct {
Type string `json:"type"`
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
PortStart uint `json:"port_start" validate:"required,gte=1,lte=65535"`
PortEnd uint `json:"port_end" validate:"required,gte=1,lte=65535"`
@@ -13,3 +14,18 @@ type FirewallRule struct {
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
Direction string `json:"direction"`
}
type FirewallIPRule struct {
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
Address string `json:"address"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
Direction string `json:"direction"`
}
type FirewallForward struct {
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
Port uint `json:"port" validate:"required,gte=1,lte=65535"`
TargetIP string `json:"target_ip" validate:"required"`
TargetPort uint `json:"target_port" validate:"required,gte=1,lte=65535"`
}
+1 -1
View File
@@ -8,7 +8,7 @@ type PanelSetting struct {
WebsitePath string `json:"website_path" validate:"required"`
BackupPath string `json:"backup_path" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password"`
Password string `json:"password" validate:"password"`
Email string `json:"email" validate:"required"`
Port int `json:"port" validate:"required,number,gte=1,lte=65535"`
HTTPS bool `json:"https"`
+21 -5
View File
@@ -1,8 +1,24 @@
package request
type SSHUpdateInfo struct {
Host string `json:"host" form:"host" validate:"required"`
Port int `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
User string `json:"user" form:"user" validate:"required"`
Password string `json:"password" form:"password" validate:"required"`
type SSHCreate struct {
Name string `json:"name" form:"name"`
Host string `json:"host" form:"host" validate:"required"`
Port uint `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required,oneof=password publickey"`
User string `json:"user" form:"user" validate:"required_if=AuthMethod password"`
Password string `json:"password" form:"password" validate:"required_if=AuthMethod password"`
Key string `json:"key" form:"key" validate:"required_if=AuthMethod publickey"`
Remark string `json:"remark" form:"remark"`
}
type SSHUpdate struct {
ID uint `form:"id" json:"id" validate:"required,exists=sshes id"`
Name string `json:"name" form:"name"`
Host string `json:"host" form:"host" validate:"required"`
Port uint `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required,oneof=password publickey"`
User string `json:"user" form:"user" validate:"required_if=AuthMethod password"`
Password string `json:"password" form:"password" validate:"required_if=AuthMethod password"`
Key string `json:"key" form:"key" validate:"required_if=AuthMethod publickey"`
Remark string `json:"remark" form:"remark"`
}
+1 -1
View File
@@ -17,7 +17,7 @@ type WebsiteCreate struct {
DBType string `form:"db_type" json:"db_type"`
DBName string `form:"db_name" json:"db_name"`
DBUser string `form:"db_user" json:"db_user"`
DBPassword string `form:"db_password" json:"db_password"`
DBPassword string `form:"db_password" json:"db_password" validate:"password"`
}
type WebsiteDelete struct {
+1
View File
@@ -0,0 +1 @@
package request
+50
View File
@@ -0,0 +1,50 @@
package rule
import (
"unicode"
"github.com/go-playground/validator/v10"
)
type Password struct{}
func NewPassword() *Password {
return &Password{}
}
// Password 密码复杂度校验
func (r *Password) Password(fl validator.FieldLevel) bool {
password := fl.Field().String()
// 不对空密码进行校验,有需要可以使用 required 标签
if password == "" {
return true
}
var hasUpper, hasLower, hasNumber, hasSpecial bool
if len(password) < 8 || len(password) > 20 {
return false
}
for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
// 至少包含两类字符组合
valid := (hasUpper && hasLower) ||
(hasUpper && hasNumber) ||
(hasUpper && hasSpecial) ||
(hasLower && hasNumber) ||
(hasLower && hasSpecial) ||
(hasNumber && hasSpecial)
return valid
}
+29
View File
@@ -0,0 +1,29 @@
package rule
import (
"regexp"
"strings"
"github.com/go-playground/validator/v10"
)
type Regex struct{}
func NewRegex() *Regex {
return &Regex{}
}
func (r *Regex) Regex(fl validator.FieldLevel) bool {
// 从标签中获取正则,格式类似于 `regex=^[a-zA-Z0-9_]+$`
pattern := fl.Param()
// 替换转义字符
pattern = strings.ReplaceAll(pattern, "", ",")
re, err := regexp.Compile(pattern)
if err != nil {
return false
}
value := fl.Field().String()
return re.MatchString(value)
}
-26
View File
@@ -1,26 +0,0 @@
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)
}
+17 -4
View File
@@ -14,7 +14,10 @@ func RegisterRules(v *validator.Validate) error {
if err := v.RegisterValidation("not_exists", NewNotExists(app.Orm).NotExists); err != nil {
return err
}
if err := v.RegisterValidation("regexp", NewRegexp().Regexp); err != nil {
if err := v.RegisterValidation("regex", NewRegex().Regex); err != nil {
return err
}
if err := v.RegisterValidation("password", NewPassword().Password); err != nil {
return err
}
@@ -38,12 +41,22 @@ func RegisterRules(v *validator.Validate) error {
}); err != nil {
return err
}
if err := v.RegisterTranslation("regexp", *app.Translator,
if err := v.RegisterTranslation("regex", *app.Translator,
func(ut ut.Translator) error {
return ut.Add("regexp", "{0} 格式不正确", true)
return ut.Add("regex", "{0} 格式不正确", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("regexp", fe.Field())
t, _ := ut.T("regex", fe.Field())
return t
}); err != nil {
return err
}
if err := v.RegisterTranslation("password", *app.Translator,
func(ut ut.Translator) error {
return ut.Add("password", "密码不满足要求(8-20位,至少包含字母、数字、特殊字符中的两种)", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("password")
return t
}); err != nil {
return err
+13
View File
@@ -42,4 +42,17 @@ func init() {
)
},
})
Migrations = append(Migrations, &gormigrate.Migration{
ID: "20241022-ssh",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
&biz.SSH{},
)
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable(
&biz.SSH{},
)
},
})
}
+12 -2
View File
@@ -26,7 +26,7 @@ func Cli() []*cli.Command {
},
{
Name: "update",
Usage: "升级面板",
Usage: "更新面板",
Action: cliService.Update,
},
{
@@ -327,6 +327,11 @@ func Cli() []*cli.Command {
Usage: "卸载应用",
Action: cliService.AppUnInstall,
},
{
Name: "update",
Usage: "更新应用",
Action: cliService.AppUpdate,
},
{
Name: "write",
Usage: "添加面板应用标记(仅限指导下使用)",
@@ -367,7 +372,12 @@ func Cli() []*cli.Command {
},
},
{
Name: "clearTask",
Name: "sync-time",
Usage: "同步系统时间",
Action: cliService.SyncTime,
},
{
Name: "clear-task",
Usage: "清理面板任务队列(仅限指导下使用)",
Hidden: true,
Action: cliService.ClearTask,
+13 -5
View File
@@ -60,6 +60,7 @@ func Http(r chi.Router) {
r.Post("/{id}/updateRemark", website.UpdateRemark)
r.Post("/{id}/resetConfig", website.ResetConfig)
r.Post("/{id}/status", website.UpdateStatus)
r.Post("/{id}/obtainCert", website.ObtainCert)
})
r.Route("/backup", func(r chi.Router) {
@@ -128,7 +129,6 @@ func Http(r chi.Router) {
r.Get("/{id}", cron.Get)
r.Delete("/{id}", cron.Delete)
r.Post("/{id}/status", cron.Status)
r.Get("/{id}/log", cron.Log)
})
r.Route("/safe", func(r chi.Router) {
@@ -148,14 +148,22 @@ func Http(r chi.Router) {
r.Get("/rule", firewall.GetRules)
r.Post("/rule", firewall.CreateRule)
r.Delete("/rule", firewall.DeleteRule)
r.Get("/ipRule", firewall.GetIPRules)
r.Post("/ipRule", firewall.CreateIPRule)
r.Delete("/ipRule", firewall.DeleteIPRule)
r.Get("/forward", firewall.GetForwards)
r.Post("/forward", firewall.CreateForward)
r.Delete("/forward", firewall.DeleteForward)
})
r.Route("/ssh", func(r chi.Router) {
r.Use(middleware.MustLogin)
ssh := service.NewSSHService()
r.Get("/info", ssh.GetInfo)
r.Post("/info", ssh.UpdateInfo)
r.Get("/session", ssh.Session)
r.Get("/", ssh.List)
r.Post("/", ssh.Create)
r.Put("/{id}", ssh.Update)
r.Get("/{id}", ssh.Get)
r.Delete("/{id}", ssh.Delete)
})
r.Route("/container", func(r chi.Router) {
@@ -226,7 +234,7 @@ func Http(r chi.Router) {
r.Post("/permission", file.Permission)
r.Post("/compress", file.Compress)
r.Post("/unCompress", file.UnCompress)
r.Post("/search", file.Search)
r.Get("/search", file.Search)
r.Get("/list", file.List)
})
+17
View File
@@ -0,0 +1,17 @@
package route
import (
"github.com/go-chi/chi/v5"
"github.com/TheTNB/panel/internal/http/middleware"
"github.com/TheTNB/panel/internal/service"
)
func Ws(r chi.Router) {
r.Route("/api/ws", func(r chi.Router) {
r.Use(middleware.MustLogin)
ws := service.NewWsService()
r.Get("/ssh", ws.Session)
r.Get("/exec", ws.Exec)
})
}
+1
View File
@@ -47,6 +47,7 @@ func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
show = installedAppMap[item.Slug].Show
}
apps = append(apps, types.AppCenter{
Icon: item.Icon,
Name: item.Name,
Description: item.Description,
Slug: item.Slug,
+4 -4
View File
@@ -55,16 +55,16 @@ func (s *CertService) CAProviders(w http.ResponseWriter, r *http.Request) {
func (s *CertService) DNSProviders(w http.ResponseWriter, r *http.Request) {
Success(w, []types.LV{
{
Label: "DNSPod",
Value: string(acme.DnsPod),
Label: "阿里云",
Value: string(acme.AliYun),
},
{
Label: "腾讯云",
Value: string(acme.Tencent),
},
{
Label: "阿里云",
Value: string(acme.AliYun),
Label: "华为云",
Value: string(acme.Huawei),
},
{
Label: "CloudFlare",
+127 -79
View File
@@ -9,7 +9,6 @@ import (
"github.com/go-rat/utils/hash"
"github.com/goccy/go-yaml"
"github.com/gookit/color"
"github.com/spf13/cast"
"github.com/urfave/cli/v3"
"gorm.io/gorm"
@@ -20,6 +19,7 @@ import (
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/api"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/ntp"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/tools"
@@ -55,7 +55,7 @@ func (s *CliService) Restart(ctx context.Context, cmd *cli.Command) error {
return err
}
color.Greenln("面板服务已重启")
fmt.Println("面板服务已重启")
return nil
}
@@ -64,7 +64,7 @@ func (s *CliService) Stop(ctx context.Context, cmd *cli.Command) error {
return err
}
color.Greenln("面板服务已停止")
fmt.Println("面板服务已停止")
return nil
}
@@ -73,7 +73,7 @@ func (s *CliService) Start(ctx context.Context, cmd *cli.Command) error {
return err
}
color.Greenln("面板服务已启动")
fmt.Println("面板服务已启动")
return nil
}
@@ -121,10 +121,7 @@ func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error {
if app.Conf.Bool("http.tls") {
protocol = "https"
}
ip, err := tools.GetPublicIP()
if err != nil {
ip = "127.0.0.1"
}
port := app.Conf.String("http.port")
if port == "" {
return fmt.Errorf("端口获取失败")
@@ -134,11 +131,32 @@ func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("入口获取失败")
}
color.Greenln(fmt.Sprintf("用户名: %s", user.Username))
color.Greenln(fmt.Sprintf("密码: %s", password))
color.Greenln(fmt.Sprintf("端口: %s", port))
color.Greenln(fmt.Sprintf("入口: %s", entrance))
color.Greenln(fmt.Sprintf("地址: %s://%s:%s%s", protocol, ip, port, entrance))
fmt.Printf("用户名: %s\n", user.Username)
fmt.Printf("密码: %s\n", password)
fmt.Printf("端口: %s\n", port)
fmt.Printf("入口: %s\n", entrance)
lv4, err := tools.GetLocalIPv4()
if err == nil {
fmt.Printf("本地IPv4地址: %s://%s:%s%s\n", protocol, lv4, port, entrance)
}
lv6, err := tools.GetLocalIPv6()
if err == nil {
fmt.Printf("本地IPv6地址: %s://[%s]:%s%s\n", protocol, lv6, port, entrance)
}
rv4, err := tools.GetPublicIPv4()
if err == nil {
fmt.Printf("公网IPv4地址: %s://%s:%s%s\n", protocol, rv4, port, entrance)
}
rv6, err := tools.GetPublicIPv6()
if err == nil {
fmt.Printf("公网IPv6地址: %s://[%s]:%s%s\n", protocol, rv6, port, entrance)
}
fmt.Println("请根据自身网络情况自行选择合适的地址访问面板")
fmt.Printf("如无法访问,请检查服务器运营商安全组和防火墙是否放行%s端口\n", port)
fmt.Println("若仍无法访问,可尝试运行 panel-cli https off 关闭面板HTTPS")
fmt.Println("警告:关闭面板HTTPS后,面板安全性将大大降低,请谨慎操作")
return nil
}
@@ -150,7 +168,7 @@ func (s *CliService) UserList(ctx context.Context, cmd *cli.Command) error {
}
for _, user := range users {
color.Greenln(fmt.Sprintf("ID: %d, 用户名: %s, 邮箱: %s, 创建日期: %s", user.ID, user.Username, user.Email, user.CreatedAt.Format("2006-01-02 15:04:05")))
fmt.Printf("ID: %d, 用户名: %s, 邮箱: %s, 创建日期: %s\n", user.ID, user.Username, user.Email, user.CreatedAt.Format("2006-01-02 15:04:05"))
}
return nil
@@ -180,7 +198,7 @@ func (s *CliService) UserName(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("用户名修改失败:%v", err)
}
color.Greenln(fmt.Sprintf("用户 %s 修改为 %s 成功", oldUsername, newUsername))
fmt.Printf("用户 %s 修改为 %s 成功\n", oldUsername, newUsername)
return nil
}
@@ -212,7 +230,7 @@ func (s *CliService) UserPassword(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("密码修改失败:%v", err)
}
color.Greenln(fmt.Sprintf("用户 %s 密码修改成功", username))
fmt.Printf("用户 %s 密码修改成功\n", username)
return nil
}
@@ -238,7 +256,7 @@ func (s *CliService) HTTPSOn(ctx context.Context, cmd *cli.Command) error {
return err
}
color.Greenln("已开启HTTPS")
fmt.Println("已开启HTTPS")
return s.Restart(ctx, cmd)
}
@@ -264,7 +282,7 @@ func (s *CliService) HTTPSOff(ctx context.Context, cmd *cli.Command) error {
return err
}
color.Greenln("已关闭HTTPS")
fmt.Println("已关闭HTTPS")
return s.Restart(ctx, cmd)
}
@@ -290,8 +308,8 @@ func (s *CliService) EntranceOn(ctx context.Context, cmd *cli.Command) error {
return err
}
color.Greenln("已开启访问入口")
color.Greenln(fmt.Sprintf("访问入口:%s", config.HTTP.Entrance))
fmt.Println("已开启访问入口")
fmt.Printf("访问入口:%s\n", config.HTTP.Entrance)
return s.Restart(ctx, cmd)
}
@@ -317,7 +335,7 @@ func (s *CliService) EntranceOff(ctx context.Context, cmd *cli.Command) error {
return err
}
color.Greenln("已关闭访问入口")
fmt.Println("已关闭访问入口")
return s.Restart(ctx, cmd)
}
@@ -348,7 +366,7 @@ func (s *CliService) Port(ctx context.Context, cmd *cli.Command) error {
return err
}
color.Greenln(fmt.Sprintf("已修改端口为 %d", port))
fmt.Printf("已修改端口为 %d\n", port)
return s.Restart(ctx, cmd)
}
@@ -367,7 +385,7 @@ func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error
return err
}
color.Greenln(fmt.Sprintf("网站 %s 创建成功", website.Name))
fmt.Printf("网站 %s 创建成功\n", website.Name)
return nil
}
@@ -384,7 +402,7 @@ func (s *CliService) WebsiteRemove(ctx context.Context, cmd *cli.Command) error
return err
}
color.Greenln(fmt.Sprintf("网站 %s 移除成功", website.Name))
fmt.Printf("网站 %s 移除成功\n", website.Name)
return nil
}
@@ -403,7 +421,7 @@ func (s *CliService) WebsiteDelete(ctx context.Context, cmd *cli.Command) error
return err
}
color.Greenln(fmt.Sprintf("网站 %s 删除成功", website.Name))
fmt.Printf("网站 %s 删除成功\n", website.Name)
return nil
}
@@ -413,47 +431,47 @@ func (s *CliService) WebsiteWrite(ctx context.Context, cmd *cli.Command) error {
}
func (s *CliService) BackupWebsite(ctx context.Context, cmd *cli.Command) error {
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
color.Greenln("|-备份类型:网站")
color.Greenln(fmt.Sprintf("|-备份目标:%s", cmd.String("name")))
fmt.Println(s.hr)
fmt.Printf("★ 开始备份 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
fmt.Println("|-备份类型:网站")
fmt.Printf("|-备份目标:%s\n", cmd.String("name"))
if err := s.backupRepo.Create(biz.BackupTypeWebsite, cmd.String("name"), cmd.String("path")); err != nil {
return fmt.Errorf("|-备份失败:%v", err)
}
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("☆ 备份成功 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
fmt.Println(s.hr)
fmt.Printf("☆ 备份成功 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
return nil
}
func (s *CliService) BackupDatabase(ctx context.Context, cmd *cli.Command) error {
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
color.Greenln("|-备份类型:数据库")
color.Greenln(fmt.Sprintf("|-数据库:%s", cmd.String("type")))
color.Greenln(fmt.Sprintf("|-备份目标:%s", cmd.String("name")))
fmt.Println(s.hr)
fmt.Printf("★ 开始备份 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
fmt.Println("|-备份类型:数据库")
fmt.Printf("|-数据库:%s\n", cmd.String("type"))
fmt.Printf("|-备份目标:%s\n", cmd.String("name"))
if err := s.backupRepo.Create(biz.BackupType(cmd.String("type")), cmd.String("name"), cmd.String("path")); err != nil {
return fmt.Errorf("|-备份失败:%v", err)
}
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("☆ 备份成功 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
fmt.Println(s.hr)
fmt.Printf("☆ 备份成功 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
return nil
}
func (s *CliService) BackupPanel(ctx context.Context, cmd *cli.Command) error {
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
color.Greenln("|-备份类型:面板")
fmt.Println(s.hr)
fmt.Printf("★ 开始备份 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
fmt.Println("|-备份类型:面板")
if err := s.backupRepo.Create(biz.BackupTypePanel, "", cmd.String("path")); err != nil {
return fmt.Errorf("|-备份失败:%v", err)
}
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("☆ 备份成功 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
fmt.Println(s.hr)
fmt.Printf("☆ 备份成功 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
return nil
}
@@ -466,18 +484,18 @@ func (s *CliService) BackupClear(ctx context.Context, cmd *cli.Command) error {
path = cmd.String("path")
}
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("★ 开始清理 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("|-清理类型:%s", cmd.String("type")))
color.Greenln(fmt.Sprintf("|-清理目标:%s", cmd.String("file")))
color.Greenln(fmt.Sprintf("|-保留份数:%d", cmd.Int("save")))
fmt.Println(s.hr)
fmt.Printf("★ 开始清理 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
fmt.Printf("|-清理类型:%s\n", cmd.String("type"))
fmt.Printf("|-清理目标:%s\n", cmd.String("file"))
fmt.Printf("|-保留份数:%d\n", cmd.Int("save"))
if err = s.backupRepo.ClearExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
return fmt.Errorf("|-清理失败:%v", err)
}
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("☆ 清理成功 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
fmt.Println(s.hr)
fmt.Printf("☆ 清理成功 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
return nil
}
@@ -491,17 +509,17 @@ func (s *CliService) CutoffWebsite(ctx context.Context, cmd *cli.Command) error
path = cmd.String("path")
}
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("★ 开始切割日志 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
color.Greenln("|-切割类型:网站")
color.Greenln(fmt.Sprintf("|-切割目标:%s", website.Name))
fmt.Println(s.hr)
fmt.Printf("★ 开始切割日志 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
fmt.Println("|-切割类型:网站")
fmt.Printf("|-切割目标:%s\n", website.Name)
if err = s.backupRepo.CutoffLog(path, filepath.Join(app.Root, "wwwlogs", website.Name+".log")); err != nil {
return err
}
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("☆ 切割成功 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
fmt.Println(s.hr)
fmt.Printf("☆ 切割成功 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
return nil
}
@@ -514,24 +532,24 @@ func (s *CliService) CutoffClear(ctx context.Context, cmd *cli.Command) error {
path = cmd.String("path")
}
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("★ 开始清理切割日志 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("|-清理类型:%s", cmd.String("type")))
color.Greenln(fmt.Sprintf("|-清理目标:%s", cmd.String("file")))
color.Greenln(fmt.Sprintf("|-保留份数:%d", cmd.Int("save")))
fmt.Println(s.hr)
fmt.Printf("★ 开始清理切割日志 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
fmt.Printf("|-清理类型:%s\n", cmd.String("type"))
fmt.Printf("|-清理目标:%s\n", cmd.String("file"))
fmt.Printf("|-保留份数:%d\n", cmd.Int("save"))
if err := s.backupRepo.ClearExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
return err
}
color.Greenln(s.hr)
color.Greenln(fmt.Sprintf("☆ 清理成功 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
fmt.Println(s.hr)
fmt.Printf("☆ 清理成功 [%s]\n", time.Now().Format(time.DateTime))
fmt.Println(s.hr)
return nil
}
func (s *CliService) AppInstall(ctx context.Context, cmd *cli.Command) error {
channel := cmd.Args().Get(0)
slug := cmd.Args().Get(1)
slug := cmd.Args().First()
channel := cmd.Args().Get(1)
if channel == "" || slug == "" {
return fmt.Errorf("参数不能为空")
}
@@ -540,7 +558,7 @@ func (s *CliService) AppInstall(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("应用安装失败:%v", err)
}
color.Greenln(fmt.Sprintf("已创建应用 %s 安装任务", slug))
fmt.Printf("应用 %s 安装完成\n", slug)
return nil
}
@@ -555,7 +573,22 @@ func (s *CliService) AppUnInstall(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("应用卸载失败:%v", err)
}
color.Greenln(fmt.Sprintf("已创建应用 %s 卸载任务", slug))
fmt.Printf("应用 %s 卸载完成\n", slug)
return nil
}
func (s *CliService) AppUpdate(ctx context.Context, cmd *cli.Command) error {
slug := cmd.Args().First()
if slug == "" {
return fmt.Errorf("参数不能为空")
}
if err := s.appRepo.Update(slug); err != nil {
return fmt.Errorf("应用更新失败:%v", err)
}
fmt.Printf("应用 %s 更新完成\n", slug)
return nil
}
@@ -597,6 +630,20 @@ func (s *CliService) AppRemove(ctx context.Context, cmd *cli.Command) error {
return nil
}
func (s *CliService) SyncTime(ctx context.Context, cmd *cli.Command) error {
now, err := ntp.Now()
if err != nil {
return err
}
if err = ntp.UpdateSystemTime(now); err != nil {
return err
}
fmt.Println("时间同步成功")
return nil
}
func (s *CliService) ClearTask(ctx context.Context, cmd *cli.Command) error {
if err := app.Orm.Model(&biz.Task{}).
Where("status", biz.TaskStatusRunning).Or("status", biz.TaskStatusWaiting).
@@ -605,6 +652,7 @@ func (s *CliService) ClearTask(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("任务清理失败:%v", err)
}
fmt.Println("任务清理成功")
return nil
}
+1 -1
View File
@@ -29,7 +29,7 @@ func (s *ContainerService) List(w http.ResponseWriter, r *http.Request) {
}
paged, total := Paginate(r, containers)
items := make([]any, len(paged))
items := make([]any, 0)
for _, item := range paged {
var name string
if len(item.Names) > 0 {
+1 -1
View File
@@ -31,7 +31,7 @@ func (s *ContainerImageService) List(w http.ResponseWriter, r *http.Request) {
paged, total := Paginate(r, images)
items := make([]any, len(paged))
items := make([]any, 0)
for _, item := range paged {
items = append(items, map[string]any{
"id": item.ID,
+1 -1
View File
@@ -30,7 +30,7 @@ func (s *ContainerNetworkService) List(w http.ResponseWriter, r *http.Request) {
paged, total := Paginate(r, networks)
items := make([]any, len(paged))
items := make([]any, 0)
for _, item := range paged {
var ipamConfig []any
for _, v := range item.IPAM.Config {
+1 -1
View File
@@ -30,7 +30,7 @@ func (s *ContainerVolumeService) List(w http.ResponseWriter, r *http.Request) {
paged, total := Paginate(r, volumes)
items := make([]any, len(paged))
items := make([]any, 0)
for _, item := range paged {
var usage any
if item.UsageData != nil {
-16
View File
@@ -114,19 +114,3 @@ func (s *CronService) Status(w http.ResponseWriter, r *http.Request) {
Success(w, nil)
}
func (s *CronService) Log(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ID](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
log, err := s.cronRepo.Log(req.ID)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, log)
}
+17 -57
View File
@@ -124,40 +124,18 @@ func (s *DashboardService) CountInfo(w http.ResponseWriter, r *http.Request) {
return
}
mysqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "mysql%")
postgresqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "postgresql%")
mysqlInstalled, _ := s.appRepo.IsInstalled("slug = ?", "mysql")
postgresqlInstalled, _ := s.appRepo.IsInstalled("slug = ?", "postgresql")
type database struct {
Name string `json:"name"`
}
var databaseCount int64
var databaseCount int
if mysqlInstalled {
rootPassword, _ := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
if err == nil {
defer mysql.Close()
if err = mysql.Ping(); err != nil {
databaseCount = -1
} else {
rows, err := mysql.Query("SHOW DATABASES")
if err != nil {
databaseCount = -1
} else {
defer rows.Close()
var databases []database
for rows.Next() {
var d database
if err := rows.Scan(&d.Name); err != nil {
continue
}
if d.Name == "information_schema" || d.Name == "performance_schema" || d.Name == "mysql" || d.Name == "sys" {
continue
}
databases = append(databases, d)
}
databaseCount = int64(len(databases))
}
databases, err := mysql.Databases()
if err == nil {
databaseCount += len(databases)
}
}
}
@@ -165,38 +143,20 @@ func (s *DashboardService) CountInfo(w http.ResponseWriter, r *http.Request) {
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
if err == nil {
defer postgres.Close()
if err = postgres.Ping(); err != nil {
databaseCount = -1
} else {
rows, err := postgres.Query("SELECT datname FROM pg_database WHERE datistemplate = false")
if err != nil {
databaseCount = -1
} else {
defer rows.Close()
var databases []database
for rows.Next() {
var d database
if err = rows.Scan(&d.Name); err != nil {
continue
}
if d.Name == "postgres" || d.Name == "template0" || d.Name == "template1" {
continue
}
databases = append(databases, d)
}
databaseCount = int64(len(databases))
}
databases, err := postgres.Databases()
if err == nil {
databaseCount += len(databases)
}
}
}
var ftpCount int64
var ftpCount int
ftpInstalled, _ := s.appRepo.IsInstalled("slug = ?", "pureftpd")
if ftpInstalled {
listRaw, err := shell.Execf("pure-pw list")
if len(listRaw) != 0 && err == nil {
listArr := strings.Split(listRaw, "\n")
ftpCount = int64(len(listArr))
ftpCount = len(listArr)
}
}
@@ -214,8 +174,8 @@ func (s *DashboardService) CountInfo(w http.ResponseWriter, r *http.Request) {
}
func (s *DashboardService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request) {
mysqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "mysql%")
postgresqlInstalled, _ := s.appRepo.IsInstalled("slug like ?", "postgresql%")
mysqlInstalled, _ := s.appRepo.IsInstalled("slug = ?", "mysql")
postgresqlInstalled, _ := s.appRepo.IsInstalled("slug = ?", "postgresql")
php, _ := s.appRepo.GetInstalledAll("slug like ?", "php%")
var phpData []types.LVInt
@@ -311,7 +271,7 @@ func (s *DashboardService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
versions, err := s.api.IntermediateVersions()
if err != nil {
Error(w, http.StatusInternalServerError, "获取升级信息失败:%v", err)
Error(w, http.StatusInternalServerError, "获取更新信息失败:%v", err)
return
}
@@ -320,12 +280,12 @@ func (s *DashboardService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
func (s *DashboardService) Update(w http.ResponseWriter, r *http.Request) {
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
Error(w, http.StatusForbidden, "离线模式下无法升级")
Error(w, http.StatusForbidden, "离线模式下无法更新")
return
}
if s.taskRepo.HasRunningTask() {
Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止升级,请稍后再试")
Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止更新,请稍后再试")
return
}
+117 -30
View File
@@ -8,13 +8,17 @@ import (
"net/http"
stdos "os"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
"time"
"github.com/go-rat/chix"
"github.com/spf13/cast"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/data"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/os"
@@ -23,10 +27,13 @@ import (
)
type FileService struct {
taskRepo biz.TaskRepo
}
func NewFileService() *FileService {
return &FileService{}
return &FileService{
taskRepo: data.NewTaskRepo(),
}
}
func (s *FileService) Create(w http.ResponseWriter, r *http.Request) {
@@ -132,7 +139,7 @@ func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) {
return
}
if io.Exists(path) {
Error(w, http.StatusForbidden, "目标路径%s已存在", path)
Error(w, http.StatusForbidden, "目标路径 %s 已存在", path)
return
}
@@ -168,7 +175,8 @@ func (s *FileService) Move(w http.ResponseWriter, r *http.Request) {
}
if io.Exists(req.Target) && !req.Force {
Error(w, http.StatusForbidden, "目标路径%s已存在", req.Target)
Error(w, http.StatusForbidden, "目标路径 %s 已存在", req.Target)
return
}
if err = io.Mv(req.Source, req.Target); err != nil {
@@ -187,7 +195,8 @@ func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) {
}
if io.Exists(req.Target) && !req.Force {
Error(w, http.StatusForbidden, "目标路径%s已存在", req.Target)
Error(w, http.StatusForbidden, "目标路径 %s 已存在", req.Target)
return
}
if err = io.Cp(req.Source, req.Target); err != nil {
@@ -221,7 +230,25 @@ func (s *FileService) Download(w http.ResponseWriter, r *http.Request) {
}
func (s *FileService) RemoteDownload(w http.ResponseWriter, r *http.Request) {
// TODO: 未实现
req, err := Bind[request.FileRemoteDownload](r)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
timestamp := time.Now().Format("20060102150405")
task := new(biz.Task)
task.Name = "下载远程文件"
task.Status = biz.TaskStatusWaiting
task.Shell = fmt.Sprintf(`wget -o /tmp/remote-download-%s.log -O '%s' '%s'`, timestamp, req.Path, req.URL)
task.Log = fmt.Sprintf("/tmp/remote-download-%s.log", timestamp)
if err = s.taskRepo.Push(task); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *FileService) Info(w http.ResponseWriter, r *http.Request) {
@@ -324,45 +351,74 @@ func (s *FileService) Search(w http.ResponseWriter, r *http.Request) {
return
}
paths := make(map[string]stdos.FileInfo)
err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error {
if err != nil {
return err
}
if strings.Contains(info.Name(), req.KeyWord) {
paths[path] = info
}
return nil
})
results, err := io.SearchX(req.Path, req.Keyword, req.Sub)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, paths)
paged, total := Paginate(r, s.formatInfo(results))
Success(w, chix.M{
"total": total,
"items": paged,
})
}
func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FilePath](r)
req, err := Bind[request.FileList](r)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
fileInfoList, err := io.ReadDir(req.Path)
list, err := io.ReadDir(req.Path)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if req.Sort == "asc" {
slices.SortFunc(list, func(a, b stdos.DirEntry) int {
return strings.Compare(strings.ToLower(b.Name()), strings.ToLower(a.Name()))
})
} else if req.Sort == "desc" {
slices.SortFunc(list, func(a, b stdos.DirEntry) int {
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
})
} else {
slices.SortFunc(list, func(a, b stdos.DirEntry) int {
if a.IsDir() && !b.IsDir() {
return -1
}
if !a.IsDir() && b.IsDir() {
return 1
}
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
})
}
paged, total := Paginate(r, s.formatDir(req.Path, list))
Success(w, chix.M{
"total": total,
"items": paged,
})
}
// formatDir 格式化目录信息
func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any {
var paths []any
for _, fileInfo := range fileInfoList {
info, _ := fileInfo.Info()
stat := info.Sys().(*syscall.Stat_t)
for _, file := range entries {
info, err := file.Info()
if err != nil {
continue
}
stat := info.Sys().(*syscall.Stat_t)
paths = append(paths, map[string]any{
"name": info.Name(),
"full": filepath.Join(req.Path, info.Name()),
"full": filepath.Join(base, info.Name()),
"size": str.FormatBytes(float64(info.Size())),
"mode_str": info.Mode().String(),
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
@@ -372,21 +428,52 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
"gid": stat.Gid,
"hidden": io.IsHidden(info.Name()),
"symlink": io.IsSymlink(info.Mode()),
"link": io.GetSymlink(filepath.Join(req.Path, info.Name())),
"link": io.GetSymlink(filepath.Join(base, info.Name())),
"dir": info.IsDir(),
"modify": info.ModTime().Format(time.DateTime),
})
}
paged, total := Paginate(r, paths)
Success(w, chix.M{
"total": total,
"items": paged,
})
return paths
}
// setPermission
// formatInfo 格式化文件信息
func (s *FileService) formatInfo(infos map[string]stdos.FileInfo) []map[string]any {
var paths []map[string]any
for path, info := range infos {
stat := info.Sys().(*syscall.Stat_t)
paths = append(paths, map[string]any{
"name": info.Name(),
"full": path,
"size": str.FormatBytes(float64(info.Size())),
"mode_str": info.Mode().String(),
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
"owner": os.GetUser(stat.Uid),
"group": os.GetGroup(stat.Gid),
"uid": stat.Uid,
"gid": stat.Gid,
"hidden": io.IsHidden(info.Name()),
"symlink": io.IsSymlink(info.Mode()),
"link": io.GetSymlink(path),
"dir": info.IsDir(),
"modify": info.ModTime().Format(time.DateTime),
})
}
slices.SortFunc(paths, func(a, b map[string]any) int {
if cast.ToBool(a["dir"]) && !cast.ToBool(b["dir"]) {
return -1
}
if !cast.ToBool(a["dir"]) && cast.ToBool(b["dir"]) {
return 1
}
return strings.Compare(strings.ToLower(cast.ToString(a["name"])), strings.ToLower(cast.ToString(b["name"])))
})
return paths
}
// setPermission 设置权限
func (s *FileService) setPermission(path string, mode stdos.FileMode, owner, group string) {
_ = io.Chmod(path, mode)
_ = io.Chown(path, owner, group)
+84 -24
View File
@@ -10,11 +10,13 @@ import (
"net/http"
stdos "os"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"github.com/go-rat/chix"
"github.com/spf13/cast"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/io"
@@ -132,7 +134,7 @@ func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) {
return
}
if io.Exists(path) {
Error(w, http.StatusForbidden, "目标路径%s已存在", path)
Error(w, http.StatusForbidden, "目标路径 %s 已存在", path)
return
}
@@ -168,7 +170,8 @@ func (s *FileService) Move(w http.ResponseWriter, r *http.Request) {
}
if io.Exists(req.Target) && !req.Force {
Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在")
Error(w, http.StatusForbidden, "目标路径 %s 已存在", req.Target)
return
}
if err = io.Mv(req.Source, req.Target); err != nil {
@@ -187,7 +190,8 @@ func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) {
}
if io.Exists(req.Target) && !req.Force {
Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在")
Error(w, http.StatusForbidden, "目标路径 %s 已存在", req.Target)
return
}
if err = io.Cp(req.Source, req.Target); err != nil {
@@ -311,44 +315,70 @@ func (s *FileService) Search(w http.ResponseWriter, r *http.Request) {
return
}
paths := make(map[string]stdos.FileInfo)
err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error {
if err != nil {
return err
}
if strings.Contains(info.Name(), req.KeyWord) {
paths[path] = info
}
return nil
})
results, err := io.SearchX(req.Path, req.Keyword, req.Sub)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, paths)
paged, total := Paginate(r, s.formatInfo(results))
Success(w, chix.M{
"total": total,
"items": paged,
})
}
func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FilePath](r)
req, err := Bind[request.FileList](r)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
fileInfoList, err := io.ReadDir(req.Path)
list, err := io.ReadDir(req.Path)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if req.Sort == "asc" {
slices.SortFunc(list, func(a, b stdos.DirEntry) int {
return strings.Compare(strings.ToLower(b.Name()), strings.ToLower(a.Name()))
})
} else if req.Sort == "desc" {
slices.SortFunc(list, func(a, b stdos.DirEntry) int {
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
})
} else {
slices.SortFunc(list, func(a, b stdos.DirEntry) int {
if a.IsDir() && !b.IsDir() {
return -1
}
if !a.IsDir() && b.IsDir() {
return 1
}
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
})
}
paged, total := Paginate(r, s.formatDir(req.Path, list))
Success(w, chix.M{
"total": total,
"items": paged,
})
}
// formatDir 格式化目录信息
func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any {
var paths []any
for _, fileInfo := range fileInfoList {
info, _ := fileInfo.Info()
for _, file := range entries {
info, _ := file.Info()
paths = append(paths, map[string]any{
"name": info.Name(),
"full": filepath.Join(req.Path, info.Name()),
"full": filepath.Join(base, info.Name()),
"size": str.FormatBytes(float64(info.Size())),
"mode_str": info.Mode().String(),
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
@@ -358,18 +388,48 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
"gid": 0,
"hidden": io.IsHidden(info.Name()),
"symlink": io.IsSymlink(info.Mode()),
"link": io.GetSymlink(filepath.Join(req.Path, info.Name())),
"link": io.GetSymlink(filepath.Join(base, info.Name())),
"dir": info.IsDir(),
"modify": info.ModTime().Format(time.DateTime),
})
}
paged, total := Paginate(r, paths)
return paths
}
Success(w, chix.M{
"total": total,
"items": paged,
// formatInfo 格式化文件信息
func (s *FileService) formatInfo(infos map[string]stdos.FileInfo) []map[string]any {
var paths []map[string]any
for path, info := range infos {
paths = append(paths, map[string]any{
"name": info.Name(),
"full": path,
"size": str.FormatBytes(float64(info.Size())),
"mode_str": info.Mode().String(),
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
"owner": "",
"group": "",
"uid": 0,
"gid": 0,
"hidden": io.IsHidden(info.Name()),
"symlink": io.IsSymlink(info.Mode()),
"link": io.GetSymlink(path),
"dir": info.IsDir(),
"modify": info.ModTime().Format(time.DateTime),
})
}
slices.SortFunc(paths, func(a, b map[string]any) int {
if cast.ToBool(a["dir"]) && !cast.ToBool(b["dir"]) {
return -1
}
if !cast.ToBool(a["dir"]) && cast.ToBool(b["dir"]) {
return 1
}
return strings.Compare(strings.ToLower(cast.ToString(a["name"])), strings.ToLower(cast.ToString(b["name"])))
})
return paths
}
// setPermission
+150 -3
View File
@@ -2,11 +2,13 @@ package service
import (
"net/http"
"slices"
"github.com/go-rat/chix"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/firewall"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/systemctl"
)
@@ -64,7 +66,39 @@ func (s *FirewallService) GetRules(w http.ResponseWriter, r *http.Request) {
return
}
paged, total := Paginate(r, rules)
var filledRules []map[string]any
for rule := range slices.Values(rules) {
// 去除IP规则
if rule.PortStart == 1 && rule.PortEnd == 65535 {
continue
}
isUse := false
for port := rule.PortStart; port <= rule.PortEnd; port++ {
if rule.Protocol == firewall.ProtocolTCP {
isUse = os.TCPPortInUse(port)
} else if rule.Protocol == firewall.ProtocolUDP {
isUse = os.UDPPortInUse(port)
} else {
isUse = os.TCPPortInUse(port) || os.UDPPortInUse(port)
}
if isUse {
break
}
}
filledRules = append(filledRules, map[string]any{
"type": rule.Type,
"family": rule.Family,
"port_start": rule.PortStart,
"port_end": rule.PortEnd,
"protocol": rule.Protocol,
"address": rule.Address,
"strategy": rule.Strategy,
"direction": rule.Direction,
"in_use": isUse,
})
}
paged, total := Paginate(r, filledRules)
Success(w, chix.M{
"total": total,
@@ -80,7 +114,7 @@ func (s *FirewallService) CreateRule(w http.ResponseWriter, r *http.Request) {
}
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,
Type: firewall.Type(req.Type), Family: req.Family, PortStart: req.PortStart, PortEnd: req.PortEnd, Protocol: firewall.Protocol(req.Protocol), Address: req.Address, Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
}, firewall.OperationAdd); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
@@ -97,7 +131,120 @@ func (s *FirewallService) DeleteRule(w http.ResponseWriter, r *http.Request) {
}
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,
Type: firewall.Type(req.Type), Family: req.Family, PortStart: req.PortStart, PortEnd: req.PortEnd, Protocol: firewall.Protocol(req.Protocol), Address: req.Address, Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
}, firewall.OperationRemove); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *FirewallService) GetIPRules(w http.ResponseWriter, r *http.Request) {
rules, err := s.firewall.ListRule()
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
var filledRules []map[string]any
for rule := range slices.Values(rules) {
// 保留IP规则
if rule.PortStart != 1 || rule.PortEnd != 65535 || rule.Address == "" {
continue
}
filledRules = append(filledRules, map[string]any{
"family": rule.Family,
"protocol": rule.Protocol,
"address": rule.Address,
"strategy": rule.Strategy,
"direction": rule.Direction,
})
}
paged, total := Paginate(r, filledRules)
Success(w, chix.M{
"total": total,
"items": paged,
})
}
func (s *FirewallService) CreateIPRule(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FirewallIPRule](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.firewall.RichRules(firewall.FireInfo{
Family: req.Family, Address: req.Address, Protocol: firewall.Protocol(req.Protocol), Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
}, firewall.OperationAdd); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *FirewallService) DeleteIPRule(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FirewallIPRule](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.firewall.RichRules(firewall.FireInfo{
Family: req.Family, Address: req.Address, Protocol: firewall.Protocol(req.Protocol), Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
}, firewall.OperationRemove); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *FirewallService) GetForwards(w http.ResponseWriter, r *http.Request) {
forwards, err := s.firewall.ListForward()
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
paged, total := Paginate(r, forwards)
Success(w, chix.M{
"total": total,
"items": paged,
})
}
func (s *FirewallService) CreateForward(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FirewallForward](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.firewall.Forward(firewall.Forward{
Protocol: firewall.Protocol(req.Protocol), Port: req.Port, TargetIP: req.TargetIP, TargetPort: req.TargetPort,
}, firewall.OperationAdd); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *FirewallService) DeleteForward(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FirewallForward](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.firewall.Forward(firewall.Forward{
Protocol: firewall.Protocol(req.Protocol), Port: req.Port, TargetIP: req.TargetIP, TargetPort: req.TargetPort,
}, firewall.OperationRemove); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
+69 -92
View File
@@ -1,19 +1,13 @@
package service
import (
"bytes"
"context"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/spf13/cast"
"github.com/go-rat/chix"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/data"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/ssh"
)
type SSHService struct {
@@ -26,99 +20,82 @@ func NewSSHService() *SSHService {
}
}
func (s *SSHService) GetInfo(w http.ResponseWriter, r *http.Request) {
info, err := s.sshRepo.GetInfo()
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, info)
}
func (s *SSHService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.SSHUpdateInfo](r)
func (s *SSHService) List(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.Paginate](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.sshRepo.UpdateInfo(req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
}
func (s *SSHService) Session(w http.ResponseWriter, r *http.Request) {
info, err := s.sshRepo.GetInfo()
cron, total, err := s.sshRepo.List(req.Page, req.Limit)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
upGrader := websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
}
ws, err := upGrader.Upgrade(w, r, nil)
if err != nil {
ErrorSystem(w)
return
}
defer ws.Close()
config := ssh.ClientConfigPassword(
cast.ToString(info["host"])+":"+cast.ToString(info["port"]),
cast.ToString(info["user"]),
cast.ToString(info["password"]),
)
client, err := ssh.NewSSHClient(config)
if err != nil {
_ = ws.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
ErrorSystem(w)
return
}
defer client.Close()
turn, err := ssh.NewTurn(ws, client)
if err != nil {
_ = ws.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
ErrorSystem(w)
return
}
defer turn.Close()
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
var logBuff = bufPool.Get().(*bytes.Buffer)
logBuff.Reset()
defer bufPool.Put(logBuff)
sshCtx, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
if err = turn.LoopRead(logBuff, sshCtx); err != nil {
ErrorSystem(w)
return
}
}()
go func() {
defer wg.Done()
if err = turn.SessionWait(); err != nil {
ErrorSystem(w)
return
}
cancel()
}()
wg.Wait()
Success(w, chix.M{
"total": total,
"items": cron,
})
}
func (s *SSHService) Create(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.SSHCreate](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.sshRepo.Create(req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *SSHService) Update(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.SSHUpdate](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.sshRepo.Update(req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *SSHService) Get(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ID](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
cron, err := s.sshRepo.Get(req.ID)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, cron)
}
func (s *SSHService) Delete(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ID](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.sshRepo.Delete(req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
-6
View File
@@ -8,7 +8,6 @@ import (
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/data"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/shell"
)
type TaskService struct {
@@ -59,11 +58,6 @@ func (s *TaskService) Get(w http.ResponseWriter, r *http.Request) {
return
}
log, err := shell.Execf(`tail -n 500 '%s'`, task.Log)
if err == nil {
task.Log = log
}
Success(w, task)
}
+15
View File
@@ -202,3 +202,18 @@ func (s *WebsiteService) UpdateStatus(w http.ResponseWriter, r *http.Request) {
Success(w, nil)
}
func (s *WebsiteService) ObtainCert(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ID](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.websiteRepo.ObtainCert(r.Context(), req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
+141
View File
@@ -0,0 +1,141 @@
package service
import (
"bufio"
"context"
"net/http"
"sync"
"github.com/gorilla/websocket"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/data"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/ssh"
)
type WsService struct {
sshRepo biz.SSHRepo
}
func NewWsService() *WsService {
return &WsService{
sshRepo: data.NewSSHRepo(),
}
}
func (s *WsService) Session(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ID](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
info, err := s.sshRepo.Get(req.ID)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
ws, err := s.upgrade(w, r)
if err != nil {
ErrorSystem(w)
return
}
defer ws.Close()
client, err := ssh.NewSSHClient(info.Config)
if err != nil {
_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return
}
defer client.Close()
turn, err := ssh.NewTurn(ws, client)
if err != nil {
_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return
}
defer turn.Close()
ctx, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
_ = turn.Handle(ctx)
}()
go func() {
defer wg.Done()
_ = turn.Wait()
}()
wg.Wait()
cancel()
}
func (s *WsService) Exec(w http.ResponseWriter, r *http.Request) {
ws, err := s.upgrade(w, r)
if err != nil {
ErrorSystem(w)
return
}
defer ws.Close()
// 第一条消息是命令
_, cmd, err := ws.ReadMessage()
if err != nil {
_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "failed to read command"))
return
}
ctx, cancel := context.WithCancel(context.Background())
out, err := shell.ExecfWithPipe(ctx, string(cmd)) // nolint: govet
if err != nil {
_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "failed to run command"))
cancel()
return
}
go func() {
scanner := bufio.NewScanner(out)
for scanner.Scan() {
line := scanner.Text()
_ = ws.WriteMessage(websocket.TextMessage, []byte(line))
}
if err = scanner.Err(); err != nil {
_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "failed to read command output"))
}
}()
s.readLoop(ws)
cancel()
}
func (s *WsService) upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
upGrader := websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
}
// debug 模式下不校验 origin,方便 vite 代理调试
if app.Conf.Bool("app.debug") {
upGrader.CheckOrigin = func(r *http.Request) bool {
return true
}
}
return upGrader.Upgrade(w, r, nil)
}
// readLoop 阻塞直到客户端关闭连接
func (s *WsService) readLoop(c *websocket.Conn) {
for {
if _, _, err := c.NextReader(); err != nil {
c.Close()
break
}
}
}
+1 -2
View File
@@ -51,11 +51,10 @@ func (c *Client) UseManualDns(total int, check ...bool) {
// UseHTTP 使用 HTTP 验证
// conf nginx 配置文件路径
// path 验证文件存放路径
func (c *Client) UseHTTP(conf, path string) {
func (c *Client) UseHTTP(conf string) {
c.zClient.ChallengeSolvers = map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: httpSolver{
conf: conf,
path: path,
},
}
}
+3 -3
View File
@@ -20,9 +20,9 @@ func (s *ClientTestSuite) TestObtainSSL() {
client, err := NewRegisterAccount(ctx, "ci@haozi.net", CALetsEncryptStaging, nil, KeyEC256)
s.Nil(err)
client.UseDns(DnsPod, DNSParam{
ID: "123456",
Token: "654321",
client.UseDns(AliYun, DNSParam{
AK: "123456",
SK: "654321",
})
/*client.UseManualDns(2)
+44 -60
View File
@@ -4,12 +4,12 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/libdns/alidns"
"github.com/libdns/cloudflare"
"github.com/libdns/dnspod"
"github.com/libdns/huaweicloud"
"github.com/libdns/libdns"
"github.com/libdns/tencentcloud"
"github.com/mholt/acmez/v2/acme"
@@ -21,35 +21,20 @@ import (
type httpSolver struct {
conf string
path string
}
func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error {
var err error
if s.path == "" {
return nil
}
challengeFilePath := filepath.Join(s.path, challenge.HTTP01ResourcePath())
if err = os.MkdirAll(filepath.Dir(challengeFilePath), 0755); err != nil {
return fmt.Errorf("无法在网站目录创建HTTP挑战所需的目录: %w", err)
}
if err = os.WriteFile(challengeFilePath, []byte(challenge.KeyAuthorization), 0644); err != nil {
return fmt.Errorf("无法在网站目录创建HTTP挑战所需的文件: %w", err)
}
conf := fmt.Sprintf(`location = /.well-known/acme-challenge/%s {
conf := fmt.Sprintf(`location = %s {
default_type text/plain;
return 200 %q;
}
`, challenge.Token, challenge.KeyAuthorization)
if err = os.WriteFile(s.conf, []byte(conf), 0644); err != nil {
return fmt.Errorf("无法写入Nginx配置文件: %w", err)
`, challenge.HTTP01ResourcePath(), challenge.KeyAuthorization)
if err := os.WriteFile(s.conf, []byte(conf), 0644); err != nil {
return fmt.Errorf("无法写入 Nginx 配置文件: %w", err)
}
if err = systemctl.Reload("nginx"); err != nil {
if err := systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return fmt.Errorf("无法重载Nginx: %w", err)
return fmt.Errorf("无法重载 Nginx: %w", err)
}
return nil
@@ -57,11 +42,6 @@ func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error {
// CleanUp cleans up the HTTP server if it is the last one to finish.
func (s httpSolver) CleanUp(_ context.Context, challenge acme.Challenge) error {
if s.path == "" {
return nil
}
_ = os.Remove(filepath.Join(s.path, challenge.HTTP01ResourcePath()))
_ = os.WriteFile(s.conf, []byte{}, 0644)
_ = systemctl.Reload("nginx")
return nil
@@ -78,11 +58,11 @@ func (s dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error
keyAuth := challenge.DNS01KeyAuthorization()
provider, err := s.getDNSProvider()
if err != nil {
return fmt.Errorf("获取DNS提供商失败: %w", err)
return fmt.Errorf("获取 DNS 提供商失败: %w", err)
}
zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName)
if err != nil {
return fmt.Errorf("获取域名%q的顶级域失败: %w", dnsName, err)
return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err)
}
rec := libdns.Record{
@@ -91,12 +71,12 @@ func (s dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error
Value: keyAuth,
}
results, err := provider.AppendRecords(ctx, zone+".", []libdns.Record{rec})
results, err := provider.SetRecords(ctx, zone+".", []libdns.Record{rec})
if err != nil {
return fmt.Errorf("域名%q添加临时记录%q失败: %w", zone, dnsName, err)
return fmt.Errorf("域名 %q 添加临时记录 %q 失败: %w", zone, dnsName, err)
}
if len(results) != 1 {
return fmt.Errorf("预期添加1条记录,但实际添加了%d条记录", len(results))
return fmt.Errorf("预期添加 1 条记录,但实际添加了 %d 条记录", len(results))
}
s.records = &results
@@ -107,13 +87,16 @@ func (s dnsSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error
dnsName := challenge.DNS01TXTRecordName()
provider, err := s.getDNSProvider()
if err != nil {
return fmt.Errorf("获取DNS提供商失败: %w", err)
return fmt.Errorf("获取 DNS 提供商失败: %w", err)
}
zone, _ := publicsuffix.EffectiveTLDPlusOne(dnsName)
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName)
if err != nil {
return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err)
}
_, _ = provider.DeleteRecords(ctx, zone+".", *s.records)
return nil
}
@@ -122,23 +105,24 @@ func (s dnsSolver) getDNSProvider() (DNSProvider, error) {
var dns DNSProvider
switch s.dns {
case DnsPod:
dns = &dnspod.Provider{
APIToken: s.param.ID + "," + s.param.Token,
case AliYun:
dns = &alidns.Provider{
AccKeyID: s.param.AK,
AccKeySecret: s.param.SK,
}
case Tencent:
dns = &tencentcloud.Provider{
SecretId: s.param.AccessKey,
SecretKey: s.param.SecretKey,
SecretId: s.param.AK,
SecretKey: s.param.SK,
}
case AliYun:
dns = &alidns.Provider{
AccKeyID: s.param.AccessKey,
AccKeySecret: s.param.SecretKey,
case Huawei:
dns = &huaweicloud.Provider{
AccessKeyId: s.param.AK,
SecretAccessKey: s.param.SK,
}
case CloudFlare:
dns = &cloudflare.Provider{
APIToken: s.param.APIkey,
APIToken: s.param.AK,
}
default:
return nil, fmt.Errorf("未知的DNS提供商 %q", s.dns)
@@ -150,22 +134,19 @@ func (s dnsSolver) getDNSProvider() (DNSProvider, error) {
type DnsType string
const (
DnsPod DnsType = "dnspod"
Tencent DnsType = "tencent"
AliYun DnsType = "aliyun"
Huawei DnsType = "huawei"
CloudFlare DnsType = "cloudflare"
)
type DNSParam struct {
ID string `form:"id" json:"id"`
Token string `form:"token" json:"token"`
AccessKey string `form:"access_key" json:"access_key"`
SecretKey string `form:"secret_key" json:"secret_key"`
APIkey string `form:"api_key" json:"api_key"`
AK string `form:"ak" json:"ak"`
SK string `form:"sk" json:"sk"`
}
type DNSProvider interface {
libdns.RecordAppender
libdns.RecordSetter
libdns.RecordDeleter
}
@@ -177,18 +158,20 @@ type manualDNSSolver struct {
}
func (s manualDNSSolver) Present(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
full := challenge.DNS01TXTRecordName()
keyAuth := challenge.DNS01KeyAuthorization()
domain, err := publicsuffix.EffectiveTLDPlusOne(full)
if err != nil {
return fmt.Errorf("获取 %q 的顶级域失败: %w", full, err)
}
*s.records = append(*s.records, DNSRecord{
Key: dnsName,
Value: keyAuth,
Name: strings.TrimSuffix(full, "."+domain),
Domain: domain,
Value: keyAuth,
})
s.dataChan <- *s.records
_, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
<-s.controlChan
return nil
}
@@ -198,6 +181,7 @@ func (s manualDNSSolver) CleanUp(_ context.Context, _ acme.Challenge) error {
}
type DNSRecord struct {
Key string `json:"key"`
Value string `json:"value"`
Name string `json:"name"`
Domain string `json:"domain"`
Value string `json:"value"`
}
+7 -3
View File
@@ -3,6 +3,7 @@ package db
import (
"database/sql"
"fmt"
"slices"
_ "github.com/go-sql-driver/mysql"
@@ -23,10 +24,10 @@ func NewMySQL(username, password, address string, typ ...string) (*MySQL, error)
}
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("初始化MySQL连接失败: %w", err)
return nil, fmt.Errorf("init mysql connection failed: %w", err)
}
if db.Ping() != nil {
return nil, fmt.Errorf("连接MySQL失败: %w", err)
if err = db.Ping(); err != nil {
return nil, fmt.Errorf("connect to mysql failed: %w", err)
}
return &MySQL{
db: db,
@@ -168,6 +169,9 @@ func (m *MySQL) Databases() ([]types.MySQLDatabase, error) {
if err := rows.Scan(&database); err != nil {
continue
}
if slices.Contains([]string{"information_schema", "performance_schema", "mysql", "sys"}, database) {
continue
}
databases = append(databases, types.MySQLDatabase{
Name: database,
})
+3 -3
View File
@@ -28,10 +28,10 @@ func NewPostgres(username, password, address, hbaFile string, port uint) (*Postg
}
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("初始化Postgres连接失败: %w", err)
return nil, fmt.Errorf("init postgres connection failed: %w", err)
}
if db.Ping() != nil {
return nil, fmt.Errorf("连接Postgres失败: %w", err)
if err = db.Ping(); err != nil {
return nil, fmt.Errorf("connect to postgres failed: %w", err)
}
return &Postgres{
db: db,
+53 -16
View File
@@ -1,26 +1,63 @@
package firewall
type Operation string
var (
OperationAdd Operation = "add" // 添加
OperationRemove Operation = "remove" // 移除
)
type Type string
var (
TypeRich Type = "rich" // rich
TypeNormal Type = "normal" // normal
)
type Protocol string
var (
ProtocolTCP Protocol = "tcp" // tcp
ProtocolUDP Protocol = "udp" // udp
ProtocolTCPUDP Protocol = "tcp/udp" // tcp/udp
)
type Strategy string
var (
StrategyAccept Strategy = "accept" // 接受
StrategyDrop Strategy = "drop" // 丢弃
StrategyReject Strategy = "reject" // 拒绝
)
type Direction string
var (
DirectionIn Direction = "in" // 传入
DirectionOut Direction = "out" // 传出
)
type FireInfo struct {
Family string `json:"family"` // ipv4 ipv6
Address string `json:"address"` // 源地址或目标地址
PortStart uint `json:"port_start"` // 1-65535
PortEnd uint `json:"port_end"` // 1-65535
Protocol string `json:"protocol"` // tcp udp tcp/udp
Strategy string `json:"strategy"` // accept drop reject
Direction string `json:"direction"` // in out 入站或出站
Type Type `json:"type"` // rich or normal
Family string `json:"family"` // ipv4 ipv6
Address string `json:"address"` // 源地址或目标地址
PortStart uint `json:"port_start"` // 1-65535
PortEnd uint `json:"port_end"` // 1-65535
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
Strategy Strategy `json:"strategy"` // accept drop reject
Direction Direction `json:"direction"` // in out 入站或出站
}
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"`
TargetPort string `json:"targetPort"` // 1-65535
Port uint `json:"port"` // 1-65535
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
TargetIP string `json:"target_ip"` // 目标地址
TargetPort uint `json:"target_port"` // 1-65535
}
type Forward struct {
Protocol string `json:"protocol"`
Port uint `json:"port"` // 1-65535
TargetIP string `json:"targetIP"`
TargetPort uint `json:"targetPort"` // 1-65535
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
Port uint `json:"port"` // 1-65535
TargetIP string `json:"target_ip"` // 目标地址
TargetPort uint `json:"target_port"` // 1-65535
}
+106 -35
View File
@@ -3,6 +3,7 @@ package firewall
import (
"errors"
"fmt"
"net"
"regexp"
"slices"
"strings"
@@ -14,13 +15,6 @@ import (
"github.com/TheTNB/panel/pkg/systemctl"
)
type Operation string
var (
OperationAdd Operation = "add"
OperationRemove Operation = "remove"
)
type Firewall struct {
forwardListRegex *regexp.Regexp
richRuleRegex *regexp.Regexp
@@ -29,7 +23,7 @@ type Firewall struct {
func NewFirewall() *Firewall {
firewall := &Firewall{
forwardListRegex: regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`),
richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)"(?: .*?(source|destination) address="([^"]+)")?(?: .*?port port="([^"]+)")?(?: .*?protocol="([^"]+)")?.*?(accept|drop|reject)$`),
richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)"(?: .*?(source|destination) address="([^"]+)")?(?: .*?port port="([^"]+)")?(?: .*?protocol(?: value)?="([^"]+)")?.*?(accept|drop|reject)$`),
}
return firewall
@@ -60,6 +54,7 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
continue
}
var item FireInfo
item.Type = TypeNormal
if strings.Contains(port, "/") {
ruleItem := strings.Split(port, "/")
portItem := strings.Split(ruleItem[0], "-")
@@ -70,7 +65,7 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
item.PortStart = cast.ToUint(ruleItem[0])
item.PortEnd = cast.ToUint(ruleItem[0])
}
item.Protocol = ruleItem[1]
item.Protocol = Protocol(ruleItem[1])
}
item.Family = "ipv4"
item.Strategy = "accept"
@@ -88,6 +83,32 @@ func (r *Firewall) ListRule() ([]FireInfo, error) {
}()
wg.Wait()
slices.SortFunc(data, func(a FireInfo, b FireInfo) int {
if a.PortStart != b.PortStart {
return int(a.PortStart - b.PortStart)
}
if a.PortEnd != b.PortEnd {
return int(a.PortEnd - b.PortEnd)
}
if a.Protocol != b.Protocol {
return strings.Compare(string(a.Protocol), string(b.Protocol))
}
if a.Family != b.Family {
return strings.Compare(a.Family, b.Family)
}
if a.Strategy != b.Strategy {
return strings.Compare(string(a.Strategy), string(b.Strategy))
}
if a.Direction != b.Direction {
return strings.Compare(string(a.Direction), string(b.Direction))
}
if a.Type != b.Type {
return strings.Compare(string(a.Type), string(b.Type))
}
return 0
})
return data, nil
}
@@ -112,13 +133,29 @@ func (r *Firewall) ListForward() ([]FireForwardInfo, error) {
}
data = append(data, FireForwardInfo{
Port: cast.ToUint(match[1]),
Protocol: match[2],
Protocol: Protocol(match[2]),
TargetIP: match[4],
TargetPort: match[3],
TargetPort: cast.ToUint(match[3]),
})
}
}
slices.SortFunc(data, func(a FireForwardInfo, b FireForwardInfo) int {
if a.Port != b.Port {
return int(a.Port - b.Port)
}
if a.TargetPort != b.TargetPort {
return int(a.TargetPort - b.TargetPort)
}
if a.Protocol != b.Protocol {
return strings.Compare(string(a.Protocol), string(b.Protocol))
}
if a.TargetIP != b.TargetIP {
return strings.Compare(a.TargetIP, b.TargetIP)
}
return 0
})
return data, nil
}
@@ -150,11 +187,15 @@ func (r *Firewall) Port(rule FireInfo, operation Operation) error {
return fmt.Errorf("invalid port range: %d-%d", rule.PortStart, rule.PortEnd)
}
// 不支持的切换使用rich rules
if (rule.Family != "" && rule.Family != "ipv4") || rule.Direction != "in" || rule.Address != "" || rule.Strategy != "accept" {
if (rule.Family != "" && rule.Family != "ipv4") || rule.Direction != "in" || rule.Address != "" || rule.Strategy != "accept" || rule.Type == TypeRich {
return r.RichRules(rule, operation)
}
protocols := strings.Split(rule.Protocol, "/")
// 未设置协议默认为tcp/udp
if rule.Protocol == "" {
rule.Protocol = ProtocolTCPUDP
}
protocols := strings.Split(string(rule.Protocol), "/")
for protocol := range slices.Values(protocols) {
stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d-%d/%s --permanent", operation, rule.PortStart, rule.PortEnd, protocol)
if err != nil {
@@ -167,7 +208,7 @@ func (r *Firewall) Port(rule FireInfo, operation Operation) error {
}
func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
protocols := strings.Split(rule.Protocol, "/")
protocols := strings.Split(string(rule.Protocol), "/")
for protocol := range slices.Values(protocols) {
var ruleBuilder strings.Builder
ruleBuilder.WriteString(fmt.Sprintf(`rule family="%s" `, rule.Family))
@@ -177,19 +218,29 @@ func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
ruleBuilder.WriteString(fmt.Sprintf(`source address="%s" `, rule.Address))
} else if rule.Direction == "out" {
ruleBuilder.WriteString(fmt.Sprintf(`destination address="%s" `, rule.Address))
} else if rule.Direction != "" {
return fmt.Errorf("invalid direction: %s", rule.Direction)
}
}
if rule.PortStart != 0 && rule.PortEnd != 0 && (rule.PortStart != 1 && rule.PortEnd != 65535) { // 1-65535是解析出来无端口规则的情况
ruleBuilder.WriteString(fmt.Sprintf(`port port="%d-%d" `, rule.PortStart, rule.PortEnd))
}
if operation == OperationRemove && protocol != "" && rule.Protocol != "tcp/udp" { // 删除操作,可以不指定协议
ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol))
ruleBuilder.WriteString(`protocol`)
if rule.PortStart == 0 && rule.PortEnd == 0 { // IP 规则下,必须添加 value
ruleBuilder.WriteString(` value`)
}
ruleBuilder.WriteString(fmt.Sprintf(`="%s" `, protocol))
}
if operation == OperationAdd && protocol != "" {
ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol))
ruleBuilder.WriteString(`protocol`)
if rule.PortStart == 0 && rule.PortEnd == 0 { // IP 规则下,必须添加 value
ruleBuilder.WriteString(` value`)
}
ruleBuilder.WriteString(fmt.Sprintf(`="%s" `, protocol))
}
ruleBuilder.WriteString(rule.Strategy)
ruleBuilder.WriteString(string(rule.Strategy))
_, err := shell.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleBuilder.String())
if err != nil {
return fmt.Errorf("%s rich rules (%s) failed, err: %v", operation, ruleBuilder.String(), err)
@@ -200,26 +251,29 @@ func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
return err
}
func (r *Firewall) PortForward(info Forward, operation Operation) error {
func (r *Firewall) Forward(rule Forward, operation Operation) error {
if err := r.enableForward(); err != nil {
return err
}
var ruleStr strings.Builder
ruleStr.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, info.Port, info.Protocol))
if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" {
ruleStr.WriteString(fmt.Sprintf("toaddr=%s:toport=%d", info.TargetIP, info.TargetPort))
} else {
ruleStr.WriteString(fmt.Sprintf("toport=%d", info.TargetPort))
}
ruleStr.WriteString(" --permanent")
protocols := strings.Split(string(rule.Protocol), "/")
for protocol := range slices.Values(protocols) {
var ruleBuilder strings.Builder
ruleBuilder.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, rule.Port, protocol))
if rule.TargetIP != "" && !r.isLocalAddress(rule.TargetIP) {
ruleBuilder.WriteString(fmt.Sprintf("toport=%d:toaddr=%s", rule.TargetPort, rule.TargetIP))
} else {
ruleBuilder.WriteString(fmt.Sprintf("toport=%d", rule.TargetPort))
}
ruleBuilder.WriteString(" --permanent")
_, err := shell.Execf(ruleStr.String()) // nolint: govet
if err != nil {
return fmt.Errorf("%s port forward failed, err: %v", operation, err)
_, err := shell.Execf(ruleBuilder.String()) // nolint: govet
if err != nil {
return fmt.Errorf("%s port forward failed, err: %v", operation, err)
}
}
_, err = shell.Execf("firewall-cmd --reload")
_, err := shell.Execf("firewall-cmd --reload")
return err
}
@@ -234,10 +288,11 @@ func (r *Firewall) parseRichRule(line string) (FireInfo, error) {
}
fireInfo := FireInfo{
Type: TypeRich,
Family: match[1],
Address: match[3],
Protocol: match[5],
Strategy: match[6],
Protocol: Protocol(match[5]),
Strategy: Strategy(match[6]),
}
if match[2] == "destination" {
@@ -271,15 +326,31 @@ func (r *Firewall) enableForward() error {
if out == "no" {
out, err = shell.Execf("firewall-cmd --zone=public --add-masquerade --permanent")
if err != nil {
return fmt.Errorf("%s: %s", err, out)
return fmt.Errorf("%v: %s", err, out)
}
_, err = shell.Execf("firewall-cmd --reload")
return err
}
return fmt.Errorf("%v: %s", err, out)
}
return nil
}
func (r *Firewall) isLocalAddress(ip string) bool {
parsed := net.ParseIP(ip)
if parsed == nil {
return false
}
if parsed.IsLoopback() {
return true
}
if parsed.IsUnspecified() {
return true
}
if strings.ToLower(ip) == "localhost" {
return true
}
return false
}
+5
View File
@@ -211,6 +211,11 @@ func GetSymlink(path string) string {
return linkPath
}
// TempFile 创建临时文件
func TempFile(dir, prefix string) (*os.File, error) {
return os.CreateTemp(dir, prefix)
}
func getFormat(f FormatArchive) archiver.CompressedArchive {
format := archiver.CompressedArchive{}
switch f {
+57 -7
View File
@@ -1,6 +1,7 @@
package io
import (
"bytes"
"fmt"
"io"
"os"
@@ -24,13 +25,13 @@ func Mkdir(path string, permission os.FileMode) error {
// Chmod 修改文件/目录权限
func Chmod(path string, permission os.FileMode) error {
cmd := exec.Command("sudo", "chmod", "-R", fmt.Sprintf("%o", permission), path)
cmd := exec.Command("chmod", "-R", fmt.Sprintf("%o", permission), path)
return cmd.Run()
}
// Chown 修改文件或目录所有者
func Chown(path, user, group string) error {
cmd := exec.Command("sudo", "chown", "-R", user+":"+group, path)
cmd := exec.Command("chown", "-R", user+":"+group, path)
return cmd.Run()
}
@@ -146,11 +147,6 @@ func TempDir(prefix string) (string, error) {
return os.MkdirTemp("", prefix)
}
// TempFile 创建临时文件
func TempFile(dir, prefix string) (*os.File, error) {
return os.CreateTemp(dir, prefix)
}
// ReadDir 读取目录
func ReadDir(path string) ([]os.DirEntry, error) {
return os.ReadDir(path)
@@ -190,3 +186,57 @@ func CountX(path string) (int64, error) {
count := len(string(out))
return int64(count), nil
}
// Search 查找文件/文件夹
func Search(path, keyword string, sub bool) (map[string]os.FileInfo, error) {
paths := make(map[string]os.FileInfo)
baseDepth := strings.Count(filepath.Clean(path), string(os.PathSeparator))
err := filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !sub && strings.Count(p, string(os.PathSeparator)) > baseDepth+1 {
return filepath.SkipDir
}
if strings.Contains(info.Name(), keyword) {
paths[p] = info
}
return nil
})
return paths, err
}
// SearchX 查找文件/文件夹(find命令)
func SearchX(path, keyword string, sub bool) (map[string]os.FileInfo, error) {
paths := make(map[string]os.FileInfo)
var cmd *exec.Cmd
if sub {
cmd = exec.Command("find", path, "-name", "*"+keyword+"*")
} else {
cmd = exec.Command("find", path, "-maxdepth", "1", "-name", "*"+keyword+"*")
}
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
info, err := os.Stat(line)
if err != nil {
return nil, err
}
paths[line] = info
}
return paths, nil
}
+15
View File
@@ -183,6 +183,21 @@ func (p *Parser) GetHTTPSRedirect() bool {
return false
}
func (p *Parser) GetAltSvc() string {
directive, err := p.FindOne("server.add_header")
if err != nil {
return ""
}
for i, param := range directive.GetParameters() {
if strings.HasPrefix(param, "Alt-Svc") && i+1 < len(directive.GetParameters()) {
return directive.GetParameters()[i+1]
}
}
return ""
}
func (p *Parser) GetAccessLog() (string, error) {
directive, err := p.FindOne("server.access_log")
if err != nil {
+11
View File
@@ -198,6 +198,17 @@ func (s *NginxTestSuite) TestHTTPSRedirect() {
s.False(parser.GetHTTPSRedirect())
}
func (s *NginxTestSuite) TestAltSvc() {
parser, err := NewParser()
s.NoError(err)
s.NoError(parser.SetHTTPS("/www/server/vhost/cert/default.pem", "/www/server/vhost/cert/default.key"))
s.Equal("", parser.GetAltSvc())
s.NoError(parser.SetAltSvc(`'h3=":$server_port"; ma=2592000'`))
s.Equal(`'h3=":$server_port"; ma=2592000'`, parser.GetAltSvc())
s.NoError(parser.SetAltSvc(""))
s.Equal("", parser.GetAltSvc())
}
func (s *NginxTestSuite) TestAccessLog() {
parser, err := NewParser()
s.NoError(err)
+39 -1
View File
@@ -418,7 +418,45 @@ func (p *Parser) SetHTTPRedirect(httpRedirect bool) error {
if !found497 && httpRedirect {
directives = append(directives, &config.Directive{
Name: "error_page",
Parameters: []string{"497", "=307", "https://$host:$server_port$request_uri"},
Parameters: []string{"497", "=308", "https://$host:$server_port$request_uri"},
})
}
return p.Set("server", directives)
}
func (p *Parser) SetAltSvc(altSvc string) 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(), "Alt-Svc") {
foundFlag = true
directives = append(directives, &config.Directive{
Name: dir.GetName(),
Parameters: []string{"Alt-Svc", altSvc},
Comment: dir.GetComment(),
})
} else {
directives = append(directives, &config.Directive{
Name: dir.GetName(),
Parameters: dir.GetParameters(),
Comment: dir.GetComment(),
})
}
}
if !foundFlag && altSvc != "" {
directives = append(directives, &config.Directive{
Name: "add_header",
Parameters: []string{"Alt-Svc", altSvc},
})
}
+2 -2
View File
@@ -47,12 +47,12 @@ func Now(address ...string) (time.Time, error) {
}
func UpdateSystemTime(time time.Time) error {
_, err := shell.Execf(`sudo date -s "%s"`, time.Format("2006-01-02 15:04:05"))
_, err := shell.Execf(`date -s '%s'`, time.Format("2006-01-02 15:04:05"))
return err
}
func UpdateSystemTimeZone(timezone string) error {
_, err := shell.Execf(`sudo timedatectl set-timezone %s`, timezone)
_, err := shell.Execf(`timedatectl set-timezone '%s'`, timezone)
return err
}
+22
View File
@@ -2,6 +2,8 @@ package os
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
@@ -58,3 +60,23 @@ func IsUbuntu() bool {
id, idLike := osRelease["ID"], osRelease["ID_LIKE"]
return id == "ubuntu" || strings.Contains(idLike, "ubuntu")
}
func TCPPortInUse(port uint) bool {
addr := fmt.Sprintf(":%d", port)
conn, err := net.Listen("tcp", addr)
if err != nil {
return true
}
defer conn.Close()
return false
}
func UDPPortInUse(port uint) bool {
addr := fmt.Sprintf(":%d", port)
conn, err := net.ListenPacket("udp", addr)
if err != nil {
return true
}
defer conn.Close()
return false
}
+55 -9
View File
@@ -2,19 +2,25 @@ package shell
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"slices"
"strings"
"time"
)
// Execf 执行 shell 命令
func Execf(shell string, args ...any) (string, error) {
var cmd *exec.Cmd
if !preCheckArg(args) {
return "", errors.New("command contains illegal characters")
}
_ = os.Setenv("LC_ALL", "C")
cmd = exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
cmd := exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
@@ -22,7 +28,7 @@ func Execf(shell string, args ...any) (string, error) {
err := cmd.Run()
if err != nil {
return "", errors.New(strings.TrimSpace(stderr.String()))
return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String()))
}
return strings.TrimSpace(stdout.String()), err
@@ -30,9 +36,12 @@ func Execf(shell string, args ...any) (string, error) {
// ExecfAsync 异步执行 shell 命令
func ExecfAsync(shell string, args ...any) error {
var cmd *exec.Cmd
if !preCheckArg(args) {
return errors.New("command contains illegal characters")
}
_ = os.Setenv("LC_ALL", "C")
cmd = exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
cmd := exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
err := cmd.Start()
if err != nil {
@@ -50,9 +59,12 @@ func ExecfAsync(shell string, args ...any) error {
// ExecfWithTimeout 执行 shell 命令并设置超时时间
func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string, error) {
var cmd *exec.Cmd
if !preCheckArg(args) {
return "", errors.New("command contains illegal characters")
}
_ = os.Setenv("LC_ALL", "C")
cmd = exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
cmd := exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
@@ -71,10 +83,10 @@ func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string,
select {
case <-time.After(timeout):
_ = cmd.Process.Kill()
return "", errors.New("执行超时")
return strings.TrimSpace(stdout.String()), errors.New("执行超时")
case err = <-done:
if err != nil {
return "", errors.New(strings.TrimSpace(stderr.String()))
return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String()))
}
}
@@ -83,6 +95,10 @@ func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string,
// ExecfWithOutput 执行 shell 命令并输出到终端
func ExecfWithOutput(shell string, args ...any) error {
if !preCheckArg(args) {
return errors.New("command contains illegal characters")
}
_ = os.Setenv("LC_ALL", "C")
cmd := exec.Command("bash", "-c", fmt.Sprintf(shell, args...))
cmd.Stdout = os.Stdout
@@ -90,3 +106,33 @@ func ExecfWithOutput(shell string, args ...any) error {
return cmd.Run()
}
// ExecfWithPipe 执行 shell 命令并返回管道
func ExecfWithPipe(ctx context.Context, shell string, args ...any) (out io.ReadCloser, err error) {
if !preCheckArg(args) {
return nil, errors.New("command contains illegal characters")
}
_ = os.Setenv("LC_ALL", "C")
cmd := exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf(shell, args...))
out, err = cmd.StdoutPipe()
if err != nil {
return
}
cmd.Stderr = cmd.Stdout
err = cmd.Start()
return
}
func preCheckArg(args []any) bool {
illegals := []any{`&`, `|`, `;`, `$`, `'`, `"`, "`", `(`, `)`, "\n", "\r", `>`, `<`}
for arg := range slices.Values(args) {
if slices.Contains(illegals, arg) {
return false
}
}
return true
}
+30 -31
View File
@@ -4,74 +4,73 @@ import (
"time"
"golang.org/x/crypto/ssh"
"github.com/TheTNB/panel/pkg/io"
)
type AuthMethod int8
type AuthMethod string
const (
PASSWORD AuthMethod = iota + 1
PUBLICKEY
PASSWORD AuthMethod = "password"
PUBLICKEY AuthMethod = "publickey"
)
type ClientConfig struct {
AuthMethod AuthMethod
HostAddr string
User string
Password string
KeyPath string
Timeout time.Duration
AuthMethod AuthMethod `json:"auth_method"`
Host string `json:"host"`
User string `json:"user"`
Password string `json:"password"`
Key string `json:"key"`
Timeout time.Duration `json:"timeout"`
}
func ClientConfigPassword(hostAddr, user, Password string) *ClientConfig {
func ClientConfigPassword(host, user, Password string) *ClientConfig {
return &ClientConfig{
Timeout: time.Second * 5,
Timeout: 10 * time.Second,
AuthMethod: PASSWORD,
HostAddr: hostAddr,
Host: host,
User: user,
Password: Password,
}
}
func ClientConfigPublicKey(hostAddr, user, keyPath string) *ClientConfig {
func ClientConfigPublicKey(host, user, key string) *ClientConfig {
return &ClientConfig{
Timeout: time.Second * 5,
Timeout: 10 * time.Second,
AuthMethod: PUBLICKEY,
HostAddr: hostAddr,
Host: host,
User: user,
KeyPath: keyPath,
Key: key,
}
}
func NewSSHClient(conf *ClientConfig) (*ssh.Client, error) {
config := &ssh.ClientConfig{
Timeout: conf.Timeout,
User: conf.User,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
func NewSSHClient(conf ClientConfig) (*ssh.Client, error) {
if conf.Timeout == 0 {
conf.Timeout = 10 * time.Second
}
config := &ssh.ClientConfig{}
config.SetDefaults()
config.Timeout = conf.Timeout
config.User = conf.User
config.HostKeyCallback = ssh.InsecureIgnoreHostKey()
switch conf.AuthMethod {
case PASSWORD:
config.Auth = []ssh.AuthMethod{ssh.Password(conf.Password)}
case PUBLICKEY:
signer, err := getKey(conf.KeyPath)
signer, err := parseKey(conf.Key)
if err != nil {
return nil, err
}
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
}
c, err := ssh.Dial("tcp", conf.HostAddr, config)
c, err := ssh.Dial("tcp", conf.Host, config) // TODO support ipv6
if err != nil {
return nil, err
}
return c, nil
}
func getKey(keyPath string) (ssh.Signer, error) {
key, err := io.Read(keyPath)
if err != nil {
return nil, err
}
func parseKey(key string) (ssh.Signer, error) {
return ssh.ParsePrivateKey([]byte(key))
}
+39 -73
View File
@@ -1,9 +1,7 @@
package ssh
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -13,29 +11,30 @@ import (
"golang.org/x/crypto/ssh"
)
const (
MsgData = '1'
MsgResize = '2'
)
type Turn struct {
StdinPipe io.WriteCloser
Session *ssh.Session
WsConn *websocket.Conn
type MessageResize struct {
Resize bool `json:"resize"`
Columns int `json:"columns"`
Rows int `json:"rows"`
}
func NewTurn(wsConn *websocket.Conn, sshClient *ssh.Client) (*Turn, error) {
sess, err := sshClient.NewSession()
type Turn struct {
stdin io.WriteCloser
session *ssh.Session
ws *websocket.Conn
}
func NewTurn(ws *websocket.Conn, client *ssh.Client) (*Turn, error) {
sess, err := client.NewSession()
if err != nil {
return nil, err
}
stdinPipe, err := sess.StdinPipe()
stdin, err := sess.StdinPipe()
if err != nil {
return nil, err
}
turn := &Turn{StdinPipe: stdinPipe, Session: sess, WsConn: wsConn}
turn := &Turn{stdin: stdin, session: sess, ws: ws}
sess.Stdout = turn
sess.Stderr = turn
@@ -44,10 +43,10 @@ func NewTurn(wsConn *websocket.Conn, sshClient *ssh.Client) (*Turn, error) {
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err := sess.RequestPty("xterm", 150, 30, modes); err != nil {
if err = sess.RequestPty("xterm", 150, 80, modes); err != nil {
return nil, err
}
if err := sess.Shell(); err != nil {
if err = sess.Shell(); err != nil {
return nil, err
}
@@ -55,7 +54,7 @@ func NewTurn(wsConn *websocket.Conn, sshClient *ssh.Client) (*Turn, error) {
}
func (t *Turn) Write(p []byte) (n int, err error) {
writer, err := t.WsConn.NextWriter(websocket.BinaryMessage)
writer, err := t.ws.NextWriter(websocket.TextMessage)
if err != nil {
return 0, err
}
@@ -65,76 +64,43 @@ func (t *Turn) Write(p []byte) (n int, err error) {
}
func (t *Turn) Close() error {
if t.Session != nil {
t.Session.Close()
if t.session != nil {
_ = t.session.Close()
}
return t.WsConn.Close()
return t.ws.Close()
}
func (t *Turn) Read(p []byte) (n int, err error) {
for {
msgType, reader, err := t.WsConn.NextReader()
if err != nil {
return 0, err
}
if msgType != websocket.BinaryMessage {
continue
}
return reader.Read(p)
}
}
func (t *Turn) LoopRead(logBuff *bytes.Buffer, context context.Context) error {
func (t *Turn) Handle(context context.Context) error {
var resize MessageResize
for {
select {
case <-context.Done():
return errors.New("LoopRead exit")
return errors.New("ssh context done exit")
default:
_, wsData, err := t.WsConn.ReadMessage()
_, data, err := t.ws.ReadMessage()
if err != nil {
return fmt.Errorf("reading webSocket message err:%s", err)
// 通常是客户端关闭连接
return fmt.Errorf("reading ws message err: %v", err)
}
body := decode(wsData[1:])
switch wsData[0] {
case MsgResize:
var args Resize
err := json.Unmarshal(body, &args)
if err != nil {
return fmt.Errorf("ssh pty resize windows err:%s", err)
}
if args.Columns > 0 && args.Rows > 0 {
if err := t.Session.WindowChange(args.Rows, args.Columns); err != nil {
return fmt.Errorf("ssh pty resize windows err:%s", err)
// 判断是否是 resize 消息
if err = json.Unmarshal(data, &resize); err == nil {
if resize.Resize && resize.Columns > 0 && resize.Rows > 0 {
if err = t.session.WindowChange(resize.Rows, resize.Columns); err != nil {
return fmt.Errorf("change window size err: %v", err)
}
}
case MsgData:
if _, err := t.StdinPipe.Write(body); err != nil {
return fmt.Errorf("StdinPipe write err:%s", err)
}
if _, err := logBuff.Write(body); err != nil {
return fmt.Errorf("logBuff write err:%s", err)
}
continue
}
if _, err = t.stdin.Write(data); err != nil {
return fmt.Errorf("writing ws message to stdin err: %v", err)
}
}
}
}
func (t *Turn) SessionWait() error {
if err := t.Session.Wait(); err != nil {
return err
}
return nil
}
func decode(p []byte) []byte {
decodeString, _ := base64.StdEncoding.DecodeString(string(p))
return decodeString
}
type Resize struct {
Columns int
Rows int
func (t *Turn) Wait() error {
return t.session.Wait()
}
+58 -7
View File
@@ -2,7 +2,10 @@
package tools
import (
"context"
"errors"
stdnet "net"
"net/http"
"slices"
"strings"
"time"
@@ -86,8 +89,8 @@ func RestartPanel() {
// IsChina 是否中国大陆
func IsChina() bool {
client := resty.New()
client.SetTimeout(5 * time.Second)
client.SetRetryCount(2)
client.SetTimeout(3 * time.Second)
client.SetRetryCount(3)
resp, err := client.R().Get("https://www.cloudflare-cn.com/cdn-cgi/trace")
if err != nil || !resp.IsSuccess() {
@@ -101,16 +104,64 @@ func IsChina() bool {
return false
}
// GetPublicIP 获取公网IP
func GetPublicIP() (string, error) {
// GetPublicIPv4 获取公网IPv4
func GetPublicIPv4() (string, error) {
client := resty.New()
client.SetTimeout(5 * time.Second)
client.SetRetryCount(2)
client.SetTimeout(3 * time.Second)
client.SetRetryCount(3)
client.SetTransport(&http.Transport{
DialContext: func(ctx context.Context, network string, addr string) (stdnet.Conn, error) {
return (&stdnet.Dialer{}).DialContext(ctx, "tcp4", addr)
},
})
resp, err := client.R().Get("https://www.cloudflare-cn.com/cdn-cgi/trace")
if err != nil || !resp.IsSuccess() {
return "", errors.New("获取公网IP失败")
return "", errors.New("failed to get public ipv4 address")
}
return strings.TrimPrefix(strings.Split(resp.String(), "\n")[2], "ip="), nil
}
// GetPublicIPv6 获取公网IPv6
func GetPublicIPv6() (string, error) {
client := resty.New()
client.SetTimeout(3 * time.Second)
client.SetRetryCount(3)
client.SetTransport(&http.Transport{
DialContext: func(ctx context.Context, network string, addr string) (stdnet.Conn, error) {
return (&stdnet.Dialer{}).DialContext(ctx, "tcp6", addr)
},
})
resp, err := client.R().Get("https://www.cloudflare-cn.com/cdn-cgi/trace")
if err != nil || !resp.IsSuccess() {
return "", errors.New("failed to get public ipv6 address")
}
return strings.TrimPrefix(strings.Split(resp.String(), "\n")[2], "ip="), nil
}
// GetLocalIPv4 获取本地IPv4
func GetLocalIPv4() (string, error) {
conn, err := stdnet.Dial("udp", "119.29.29.29:53")
if err != nil {
return "", err
}
defer conn.Close()
local := conn.LocalAddr().(*stdnet.UDPAddr)
return local.IP.String(), nil
}
// GetLocalIPv6 获取本地IPv6
func GetLocalIPv6() (string, error) {
conn, err := stdnet.Dial("udp", "[2402:4e00::]:53")
if err != nil {
return "", err
}
defer conn.Close()
local := conn.LocalAddr().(*stdnet.UDPAddr)
return local.IP.String(), nil
}
+21 -3
View File
@@ -18,8 +18,26 @@ func (s *HelperTestSuite) TestGetMonitoringInfo() {
s.NotNil(CurrentInfo(nil, nil))
}
func (s *HelperTestSuite) TestGetPublicIP() {
ip, err := GetPublicIP()
s.Nil(err)
func (s *HelperTestSuite) TestGetPublicIPv4() {
ip, err := GetPublicIPv4()
s.NoError(err)
s.NotEmpty(ip)
}
func (s *HelperTestSuite) TestGetPublicIPv6() {
ip, err := GetPublicIPv6()
s.Error(err)
s.Empty(ip)
}
func (s *HelperTestSuite) TestGetLocalIPv4() {
ip, err := GetLocalIPv4()
s.NoError(err)
s.NotEmpty(ip)
}
func (s *HelperTestSuite) TestGetLocalIPv6() {
ip, err := GetLocalIPv6()
s.Error(err)
s.Empty(ip)
}
+1
View File
@@ -10,6 +10,7 @@ type App struct {
// AppCenter 应用中心结构
type AppCenter struct {
Icon string `json:"icon"`
Name string `json:"name"`
Description string `json:"description"`
Slug string `json:"slug"`
+8 -5
View File
@@ -1,17 +1,20 @@
import type { ProxyOptions } from 'vite'
import { getProxyConfig } from '../../settings/proxy-config'
import { getProxyConfigs } from '../../settings/proxy-config'
export function createViteProxy(isUseProxy = true, proxyType: ProxyType) {
if (!isUseProxy) return undefined
const proxyConfig = getProxyConfig(proxyType)
const proxy: Record<string, string | ProxyOptions> = {
[proxyConfig.prefix]: {
const proxyConfigs = getProxyConfigs(proxyType)
const proxy: Record<string, string | ProxyOptions> = {}
proxyConfigs.forEach((proxyConfig) => {
proxy[proxyConfig.prefix] = {
target: proxyConfig.target,
secure: proxyConfig.secure,
changeOrigin: true,
rewrite: (path: string) => path.replace(new RegExp(`^${proxyConfig.prefix}`), '')
}
}
})
return proxy
}

Some files were not shown because too many files have changed in this diff Show More