mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7c586e05b | |||
| 96c78b6be9 | |||
| 46ae37a4a1 | |||
| d56b41bfef | |||
| 68beffff22 | |||
| a774253ec3 | |||
| 9dfc1d3058 | |||
| 049f708cae | |||
| b749a8599f | |||
| 95cf05879d | |||
| 9ac5ae4968 | |||
| 2491f34b3f | |||
| 302d0f1bec | |||
| 5ac43df821 | |||
| 8a1e7ef88b | |||
| b0a4df7ca6 | |||
| 70f41b87ea | |||
| ea96634318 | |||
| cfff459673 | |||
| bba50692b3 | |||
| 595cb03c67 | |||
| fa5817febd | |||
| 9a9fdbdca6 | |||
| 2225e59ca6 | |||
| 5dce30523b | |||
| 965c39ee08 | |||
| 3507517d5b | |||
| 17cce7ec2a | |||
| a2b30b07bb | |||
| ffb7bfda51 | |||
| d36dd8fded | |||
| a5f69db353 | |||
| 4606c63c81 | |||
| 247000718d | |||
| aedde0d433 | |||
| 0b61f2f67c | |||
| b166022cea | |||
| fb88060824 | |||
| f760dbaf80 | |||
| 6050bf611b | |||
| 0b425f09ee | |||
| f18c39ff60 | |||
| 9db30ac11b | |||
| 3c8f606d45 | |||
| 59c08f0850 | |||
| 29a7aa4ca5 | |||
| 8eb75a921e | |||
| 86fb1c392e | |||
| df4e47cc7b | |||
| 6101bedee3 | |||
| 817e86466c | |||
| 710601600a | |||
| 63a934510e | |||
| f3aac307d0 | |||
| e32c887bd7 | |||
| bc37511b15 | |||
| 72652eb814 | |||
| 6e3db107ec | |||
| 81843470b7 | |||
| a539532ae2 | |||
| fc1584e7da | |||
| 3c72a66c1f | |||
| 346ec7ab1f | |||
| 7850b74966 | |||
| f2d2bad52f | |||
| 0786451c79 | |||
| 0291ae7637 | |||
| de76376e97 | |||
| de21179d61 | |||
| b81f3de01d | |||
| be3bae7d57 | |||
| ffb4e21f48 | |||
| 39624d8e1c | |||
| 1378413e54 | |||
| 730aea037c | |||
| 5ae6135429 | |||
| 84703391a3 | |||
| 354705ec5b | |||
| f182eab2dc | |||
| 2828795fb0 | |||
| e6aad21631 | |||
| ff239c467a | |||
| 892f91be3e | |||
| d51d11f460 | |||
| f9233bd36b | |||
| a2ebc070ae | |||
| 480a6a829d | |||
| 47b92a8b2a | |||
| 1a7f679fca | |||
| 8b6b94ea1f | |||
| 0d9c8b3a2d | |||
| 2be97a4543 | |||
| 7b6b86f0f6 | |||
| 6f82242d92 | |||
| 7f301cec58 | |||
| c75cfe9eb6 | |||
| b94664ed87 | |||
| 93fc5e08f6 | |||
| 9bcfd4cd73 | |||
| 2d10110eef | |||
| 78e31fdc40 | |||
| d2b1394950 | |||
| 389b0a005b | |||
| 0b1554c2c6 | |||
| 3b580e2827 | |||
| 595f5ba2d9 | |||
| 78543dac67 | |||
| e9cbc8e945 | |||
| 905e206f9c | |||
| b5b1992e3b | |||
| a7fe0c87a5 | |||
| 5dcbac1c93 | |||
| 67a6d5cea8 | |||
| f6fec06e82 | |||
| 2d74e8966e | |||
| 065877547e | |||
| 12b6a1af35 | |||
| b36f7f3085 | |||
| 2112e86329 | |||
| f2c3569447 | |||
| 1a9fe40b80 |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,4 +21,4 @@ jobs:
|
||||
run: |
|
||||
cp config.example.yml config.yml
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
run: sudo go test ./...
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "benchmark",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Post("/test", service.Test)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package benchmark
|
||||
|
||||
type Test struct {
|
||||
Name string `json:"name" validate:"required,oneof=image machine compile encryption compression physics json memory disk"`
|
||||
Multi bool `json:"multi"`
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// Test 运行测试
|
||||
func (s *Service) Test(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Test](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Name {
|
||||
case "image":
|
||||
result := s.imageProcessing(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "machine":
|
||||
result := s.machineLearning(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "compile":
|
||||
result := s.compileSimulationSingle(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "encryption":
|
||||
result := s.encryptionTest(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "compression":
|
||||
result := s.compressionTest(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "physics":
|
||||
result := s.physicsSimulation(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "json":
|
||||
result := s.jsonProcessing(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "disk":
|
||||
result := s.diskTestTask()
|
||||
service.Success(w, result)
|
||||
case "memory":
|
||||
result := s.memoryTestTask()
|
||||
service.Success(w, result)
|
||||
default:
|
||||
service.Error(w, http.StatusUnprocessableEntity, "未知测试类型")
|
||||
}
|
||||
}
|
||||
|
||||
// calculateCpuScore 计算CPU成绩
|
||||
func (s *Service) calculateCpuScore(duration time.Duration) int {
|
||||
score := int((10 / duration.Seconds()) * float64(3000))
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// calculateScore 计算内存/硬盘成绩
|
||||
func (s *Service) calculateScore(duration time.Duration) int {
|
||||
score := int((20 / duration.Seconds()) * float64(30000))
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// 图像处理
|
||||
|
||||
func (s *Service) imageProcessing(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
if err := s.imageProcessingTask(n); err != nil {
|
||||
return 0
|
||||
}
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) imageProcessingTask(numThreads int) error {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 4000, 4000))
|
||||
for x := 0; x < 4000; x++ {
|
||||
for y := 0; y < 4000; y++ {
|
||||
img.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), A: 255})
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
dx := img.Bounds().Dx()
|
||||
dy := img.Bounds().Dy()
|
||||
chunkSize := dy / numThreads
|
||||
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
startY := i * chunkSize
|
||||
endY := startY + chunkSize
|
||||
if i == numThreads-1 {
|
||||
endY = dy
|
||||
}
|
||||
for x := 1; x < dx-1; x++ {
|
||||
for y := startY + 1; y < endY-1; y++ {
|
||||
// 卷积操作(模糊)
|
||||
rTotal, gTotal, bTotal := 0, 0, 0
|
||||
for k := -1; k <= 1; k++ {
|
||||
for l := -1; l <= 1; l++ {
|
||||
r, g, b, _ := img.At(x+k, y+l).RGBA()
|
||||
rTotal += int(r)
|
||||
gTotal += int(g)
|
||||
bTotal += int(b)
|
||||
}
|
||||
}
|
||||
rAvg := uint8(rTotal / 9 / 256)
|
||||
gAvg := uint8(gTotal / 9 / 256)
|
||||
bAvg := uint8(bTotal / 9 / 256)
|
||||
img.Set(x, y, color.RGBA{R: rAvg, G: gAvg, B: bAvg, A: 255})
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 机器学习(矩阵乘法)
|
||||
|
||||
func (s *Service) machineLearning(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.machineLearningTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) machineLearningTask(numThreads int) {
|
||||
size := 900
|
||||
a := make([][]float64, size)
|
||||
b := make([][]float64, size)
|
||||
for i := 0; i < size; i++ {
|
||||
a[i] = make([]float64, size)
|
||||
b[i] = make([]float64, size)
|
||||
for j := 0; j < size; j++ {
|
||||
a[i][j] = rand.Float64()
|
||||
b[i][j] = rand.Float64()
|
||||
}
|
||||
}
|
||||
|
||||
c := make([][]float64, size)
|
||||
for i := 0; i < size; i++ {
|
||||
c[i] = make([]float64, size)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
chunkSize := size / numThreads
|
||||
|
||||
for k := 0; k < numThreads; k++ {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
start := k * chunkSize
|
||||
end := start + chunkSize
|
||||
if k == numThreads-1 {
|
||||
end = size
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
for j := 0; j < size; j++ {
|
||||
sum := 0.0
|
||||
for l := 0; l < size; l++ {
|
||||
sum += a[i][l] * b[l][j]
|
||||
}
|
||||
c[i][j] = sum
|
||||
}
|
||||
}
|
||||
}(k)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 数学问题(计算斐波那契数)
|
||||
|
||||
func (s *Service) compileSimulationSingle(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
totalCalculations := 1000
|
||||
fibNumber := 20000
|
||||
|
||||
calculationsPerThread := totalCalculations / n
|
||||
remainder := totalCalculations % n
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
tasks := calculationsPerThread
|
||||
if i < remainder {
|
||||
tasks++ // 处理无法均分的剩余任务
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(tasks int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < tasks; j++ {
|
||||
s.fib(fibNumber)
|
||||
}
|
||||
}(tasks)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
// 斐波那契函数
|
||||
func (s *Service) fib(n int) *big.Int {
|
||||
if n < 2 {
|
||||
return big.NewInt(int64(n))
|
||||
}
|
||||
a := big.NewInt(0)
|
||||
b := big.NewInt(1)
|
||||
temp := big.NewInt(0)
|
||||
for i := 2; i <= n; i++ {
|
||||
temp.Add(a, b)
|
||||
a.Set(b)
|
||||
b.Set(temp)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// AES加密
|
||||
|
||||
func (s *Service) encryptionTest(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
if err := s.encryptionTestTask(n); err != nil {
|
||||
return 0
|
||||
}
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) encryptionTestTask(numThreads int) error {
|
||||
key := []byte("abcdefghijklmnopqrstuvwxyz123456")
|
||||
dataSize := 1 * 1024 * 1024 * 1024 // 1GB
|
||||
plaintext := []byte(strings.Repeat("A", dataSize))
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chunkSize := dataSize / numThreads
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
if i == numThreads-1 {
|
||||
end = dataSize
|
||||
}
|
||||
|
||||
nonce := make([]byte, aesGCM.NonceSize())
|
||||
if _, err = cryptorand.Read(nonce); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
aesGCM.Seal(nil, nonce, plaintext[start:end], nil)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 压缩/解压缩
|
||||
|
||||
func (s *Service) compressionTest(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.compressionTestTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) compressionTestTask(numThreads int) {
|
||||
data := []byte(strings.Repeat("耗子面板", 50000000))
|
||||
chunkSize := len(data) / numThreads
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
compressedChunks := make([]bytes.Buffer, numThreads)
|
||||
|
||||
// 压缩
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
if i == numThreads-1 {
|
||||
end = len(data)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
zw, _ := zstd.NewWriter(&buf)
|
||||
_, _ = zw.Write(data[start:end])
|
||||
_ = zw.Close()
|
||||
compressedChunks[i] = buf
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 解压缩
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
zr, err := zstd.NewReader(&compressedChunks[i])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(io.Discard, zr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
zr.Close()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 物理仿真(N体问题)
|
||||
|
||||
func (s *Service) physicsSimulation(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.physicsSimulationTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) physicsSimulationTask(numThreads int) {
|
||||
const (
|
||||
numBodies = 4000
|
||||
steps = 30
|
||||
)
|
||||
|
||||
type Body struct {
|
||||
x, y, z, vx, vy, vz float64
|
||||
}
|
||||
|
||||
bodies := make([]Body, numBodies)
|
||||
for i := 0; i < numBodies; i++ {
|
||||
bodies[i] = Body{
|
||||
x: rand.Float64(),
|
||||
y: rand.Float64(),
|
||||
z: rand.Float64(),
|
||||
vx: rand.Float64(),
|
||||
vy: rand.Float64(),
|
||||
vz: rand.Float64(),
|
||||
}
|
||||
}
|
||||
|
||||
chunkSize := numBodies / numThreads
|
||||
|
||||
for step := 0; step < steps; step++ {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 更新速度
|
||||
for k := 0; k < numThreads; k++ {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
start := k * chunkSize
|
||||
end := start + chunkSize
|
||||
if k == numThreads-1 {
|
||||
end = numBodies
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
bi := &bodies[i]
|
||||
for j := 0; j < numBodies; j++ {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
bj := &bodies[j]
|
||||
dx := bj.x - bi.x
|
||||
dy := bj.y - bi.y
|
||||
dz := bj.z - bi.z
|
||||
dist := math.Sqrt(dx*dx + dy*dy + dz*dz)
|
||||
if dist == 0 {
|
||||
continue
|
||||
}
|
||||
force := 1 / (dist * dist)
|
||||
bi.vx += force * dx / dist
|
||||
bi.vy += force * dy / dist
|
||||
bi.vz += force * dz / dist
|
||||
}
|
||||
}
|
||||
}(k)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 更新位置
|
||||
for k := 0; k < numThreads; k++ {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
start := k * chunkSize
|
||||
end := start + chunkSize
|
||||
if k == numThreads-1 {
|
||||
end = numBodies
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
bi := &bodies[i]
|
||||
bi.x += bi.vx
|
||||
bi.y += bi.vy
|
||||
bi.z += bi.vz
|
||||
}
|
||||
}(k)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// JSON解析
|
||||
|
||||
func (s *Service) jsonProcessing(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.jsonProcessingTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) jsonProcessingTask(numThreads int) {
|
||||
numElements := 1000000
|
||||
elementsPerThread := numElements / numThreads
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
start := i * elementsPerThread
|
||||
end := start + elementsPerThread
|
||||
if i == numThreads-1 {
|
||||
end = numElements
|
||||
}
|
||||
|
||||
elements := make([]map[string]any, 0, end-start)
|
||||
for j := start; j < end; j++ {
|
||||
elements = append(elements, map[string]any{
|
||||
"id": j,
|
||||
"value": fmt.Sprintf("Value%d", j),
|
||||
})
|
||||
}
|
||||
encoded, err := json.Marshal(elements)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var parsed []map[string]any
|
||||
err = json.Unmarshal(encoded, &parsed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 内存性能
|
||||
|
||||
func (s *Service) memoryTestTask() map[string]any {
|
||||
results := make(map[string]any)
|
||||
dataSize := 500 * 1024 * 1024 // 500 MB
|
||||
data := make([]byte, dataSize)
|
||||
_, _ = cryptorand.Read(data)
|
||||
|
||||
start := time.Now()
|
||||
// 内存读写速度
|
||||
results["bandwidth"] = s.memoryBandwidthTest(data)
|
||||
// 内存访问延迟
|
||||
data = data[:100*1024*1024] // 100 MB
|
||||
results["latency"] = s.memoryLatencyTest(data)
|
||||
duration := time.Since(start)
|
||||
results["score"] = s.calculateScore(duration)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s *Service) memoryBandwidthTest(data []byte) string {
|
||||
dataSize := len(data)
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
for i := 0; i < dataSize; i++ {
|
||||
data[i] ^= 0xFF
|
||||
}
|
||||
|
||||
duration := time.Since(startTime).Seconds()
|
||||
if duration == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
speed := float64(dataSize) / duration / (1024 * 1024)
|
||||
return fmt.Sprintf("%.2f MB/s", speed)
|
||||
}
|
||||
|
||||
func (s *Service) memoryLatencyTest(data []byte) string {
|
||||
dataSize := len(data)
|
||||
indices := rand.Perm(dataSize)
|
||||
|
||||
startTime := time.Now()
|
||||
sum := byte(0)
|
||||
for _, idx := range indices {
|
||||
sum ^= data[idx]
|
||||
}
|
||||
duration := time.Since(startTime).Seconds()
|
||||
if duration == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
avgLatency := duration * 1e9 / float64(dataSize)
|
||||
return fmt.Sprintf("%.2f ns", avgLatency)
|
||||
}
|
||||
|
||||
// 硬盘IO
|
||||
|
||||
func (s *Service) diskTestTask() map[string]any {
|
||||
results := make(map[string]any)
|
||||
blockSizes := []int64{4 * 1024, 64 * 1024, 512 * 1024, 1 * 1024 * 1024} // 4K, 64K, 512K, 1M
|
||||
fileSize := int64(100 * 1024 * 1024) // 100MB 文件
|
||||
|
||||
start := time.Now()
|
||||
for _, blockSize := range blockSizes {
|
||||
result := s.diskIOTest(blockSize, fileSize)
|
||||
results[fmt.Sprintf("%d", blockSize/1024)] = result
|
||||
}
|
||||
duration := time.Since(start)
|
||||
results["score"] = s.calculateScore(duration)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s *Service) diskIOTest(blockSize int64, fileSize int64) map[string]any {
|
||||
result := make(map[string]any)
|
||||
tempFile := fmt.Sprintf("tempfile_%d", blockSize)
|
||||
defer os.Remove(tempFile)
|
||||
|
||||
// 写测试
|
||||
writeSpeed, writeIOPS := s.diskWriteTest(tempFile, blockSize, fileSize)
|
||||
// 读测试
|
||||
readSpeed, readIOPS := s.diskReadTest(tempFile, blockSize, fileSize)
|
||||
|
||||
result["write_speed"] = fmt.Sprintf("%.2f MB/s", writeSpeed)
|
||||
result["write_iops"] = fmt.Sprintf("%.2f IOPS", writeIOPS)
|
||||
result["read_speed"] = fmt.Sprintf("%.2f MB/s", readSpeed)
|
||||
result["read_iops"] = fmt.Sprintf("%.2f IOPS", readIOPS)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) diskWriteTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
|
||||
totalBlocks := fileSize / blockSize
|
||||
|
||||
data := make([]byte, blockSize)
|
||||
_, _ = cryptorand.Read(data)
|
||||
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_SYNC, 0644)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for i := int64(0); i < totalBlocks; i++ {
|
||||
// 生成随机偏移
|
||||
offset := rand.Int64N(fileSize - blockSize + 1)
|
||||
_, err := file.WriteAt(data, offset)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
_ = file.Sync()
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
if duration == 0 {
|
||||
duration = 1
|
||||
}
|
||||
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
|
||||
iops := float64(totalBlocks) / duration
|
||||
return speed, iops
|
||||
}
|
||||
|
||||
func (s *Service) diskReadTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
|
||||
totalBlocks := fileSize / blockSize
|
||||
|
||||
data := make([]byte, blockSize)
|
||||
|
||||
file, err := os.OpenFile(fileName, os.O_RDONLY|os.O_SYNC, 0644)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for i := int64(0); i < totalBlocks; i++ {
|
||||
// 生成随机偏移
|
||||
offset := rand.Int64N(fileSize - blockSize + 1)
|
||||
_, err := file.ReadAt(data, offset)
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
if duration == 0 {
|
||||
duration = 1
|
||||
}
|
||||
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
|
||||
iops := float64(totalBlocks) / duration
|
||||
return speed, iops
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package apps
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
_ "github.com/TheTNB/panel/internal/apps/benchmark"
|
||||
_ "github.com/TheTNB/panel/internal/apps/fail2ban"
|
||||
_ "github.com/TheTNB/panel/internal/apps/frp"
|
||||
_ "github.com/TheTNB/panel/internal/apps/gitea"
|
||||
|
||||
@@ -5,5 +5,5 @@ type UpdateConfig struct {
|
||||
}
|
||||
|
||||
type SetRootPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
}
|
||||
|
||||
@@ -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 清空慢查询日志
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,7 +2,7 @@ package pureftpd
|
||||
|
||||
type Create struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Delete struct {
|
||||
|
||||
type ChangePassword struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
|
||||
@@ -156,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 清空进程日志
|
||||
|
||||
@@ -18,6 +18,10 @@ func init() {
|
||||
r.Post("/swap", service.UpdateSWAP)
|
||||
r.Get("/timezone", service.GetTimezone)
|
||||
r.Post("/timezone", service.UpdateTimezone)
|
||||
r.Post("/time", service.UpdateTime)
|
||||
r.Post("/syncTime", service.SyncTime)
|
||||
r.Get("/hostname", service.GetHostname)
|
||||
r.Post("/hostname", service.UpdateHostname)
|
||||
r.Get("/hosts", service.GetHosts)
|
||||
r.Post("/hosts", service.UpdateHosts)
|
||||
r.Post("/rootPassword", service.UpdateRootPassword)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package toolbox
|
||||
|
||||
import "time"
|
||||
|
||||
type DNS struct {
|
||||
DNS1 string `form:"dns1" json:"dns1" validate:"required"`
|
||||
DNS2 string `form:"dns2" json:"dns2" validate:"required"`
|
||||
@@ -13,10 +15,18 @@ type Timezone struct {
|
||||
Timezone string `form:"timezone" json:"timezone" validate:"required"`
|
||||
}
|
||||
|
||||
type Time struct {
|
||||
Time time.Time `form:"time" json:"time" validate:"required"`
|
||||
}
|
||||
|
||||
type Hostname struct {
|
||||
Hostname string `form:"hostname" json:"hostname" validate:"required,regex=^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$"`
|
||||
}
|
||||
|
||||
type Hosts struct {
|
||||
Hosts string `form:"hosts" json:"hosts"`
|
||||
}
|
||||
|
||||
type Password struct {
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/ntp"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
@@ -221,6 +222,64 @@ func (s *Service) UpdateTimezone(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateTime 设置时间
|
||||
func (s *Service) UpdateTime(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Time](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ntp.UpdateSystemTime(req.Time); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
|
||||
}
|
||||
|
||||
// SyncTime 同步时间
|
||||
func (s *Service) SyncTime(w http.ResponseWriter, r *http.Request) {
|
||||
now, err := ntp.Now()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ntp.UpdateSystemTime(now); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetHostname 获取主机名
|
||||
func (s *Service) GetHostname(w http.ResponseWriter, r *http.Request) {
|
||||
hostname, _ := io.Read("/etc/hostname")
|
||||
service.Success(w, strings.TrimSpace(hostname))
|
||||
}
|
||||
|
||||
// UpdateHostname 设置主机名
|
||||
func (s *Service) UpdateHostname(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Hostname](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("hostnamectl set-hostname '%s'", req.Hostname); err != nil {
|
||||
// 直接写 /etc/hostname
|
||||
if err = io.Write("/etc/hostname", req.Hostname, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "写入主机名失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetHosts 获取 hosts 信息
|
||||
func (s *Service) GetHosts(w http.ResponseWriter, r *http.Request) {
|
||||
hosts, _ := io.Read("/etc/hosts")
|
||||
@@ -251,11 +310,6 @@ func (s *Service) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(req.Password) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符")
|
||||
return
|
||||
}
|
||||
|
||||
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
|
||||
if _, err = shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
// 初始化时区
|
||||
|
||||
@@ -23,6 +23,7 @@ func initHttp() {
|
||||
|
||||
// add route
|
||||
route.Http(app.Http)
|
||||
route.Ws(app.Http)
|
||||
apps.Boot(app.Http)
|
||||
|
||||
srv := &http.Server{
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package request
|
||||
@@ -0,0 +1,50 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type Password struct{}
|
||||
|
||||
func NewPassword() *Password {
|
||||
return &Password{}
|
||||
}
|
||||
|
||||
// Password 密码复杂度校验
|
||||
func (r *Password) Password(fl validator.FieldLevel) bool {
|
||||
password := fl.Field().String()
|
||||
// 不对空密码进行校验,有需要可以使用 required 标签
|
||||
if password == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
var hasUpper, hasLower, hasNumber, hasSpecial bool
|
||||
if len(password) < 8 || len(password) > 20 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, char := range password {
|
||||
switch {
|
||||
case unicode.IsUpper(char):
|
||||
hasUpper = true
|
||||
case unicode.IsLower(char):
|
||||
hasLower = true
|
||||
case unicode.IsNumber(char):
|
||||
hasNumber = true
|
||||
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
||||
hasSpecial = true
|
||||
}
|
||||
}
|
||||
|
||||
// 至少包含两类字符组合
|
||||
valid := (hasUpper && hasLower) ||
|
||||
(hasUpper && hasNumber) ||
|
||||
(hasUpper && hasSpecial) ||
|
||||
(hasLower && hasNumber) ||
|
||||
(hasLower && hasSpecial) ||
|
||||
(hasNumber && hasSpecial)
|
||||
|
||||
return valid
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type Regex struct{}
|
||||
|
||||
func NewRegex() *Regex {
|
||||
return &Regex{}
|
||||
}
|
||||
|
||||
func (r *Regex) Regex(fl validator.FieldLevel) bool {
|
||||
// 从标签中获取正则,格式类似于 `regex=^[a-zA-Z0-9_]+$`
|
||||
pattern := fl.Param()
|
||||
// 替换转义字符
|
||||
pattern = strings.ReplaceAll(pattern, ",", ",")
|
||||
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
value := fl.Field().String()
|
||||
return re.MatchString(value)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ type App struct {
|
||||
|
||||
// AppCenter 应用中心结构
|
||||
type AppCenter struct {
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
|
||||
@@ -1,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
Reference in New Issue
Block a user