Compare commits

...
98 Commits
Author SHA1 Message Date
CityFun 20b89a8fdd fix: update MdEditor component to prevent potential XSS attacks (#11548) 2026-01-04 09:31:43 +00:00
CityFun 8def011e63 fix: update MdEditor component to prevent potential XSS attacks (#11538) 2026-01-04 02:51:41 +00:00
J.Knight 7c5b2f41e3 feat: add support for Porkbun DNS provider (#11233)
* update: ssl add porkbun platform

* fix: some noise
2025-12-08 16:39:27 +08:00
王贺 abd3c24562 fix: update node & go version (#11195) 2025-12-04 23:18:39 +08:00
王贺 f4e64ca8b3 fix: upgrade vite and vue plugin versions (#11193)
fix: upgrade vite and vue plugin versions
2025-12-04 23:10:11 +08:00
王贺 97fde371d9 Update download URLs to use versioned paths (#11192) 2025-12-04 22:10:06 +08:00
ssongliu 85bbb9cb15 fix: Fix partial abnormal issues caused by local backup path modification (#11190) 2025-12-04 22:04:38 +08:00
ssongliu 1d34b4aff7 perf: optimize login API logic (#11187) 2025-12-04 10:06:22 +00:00
ssongliu dc8c50c07a fix: optimize IP whitelist validation logic (#11183) 2025-12-04 17:21:45 +08:00
CityFun 4ac490c6b4 fix: Fixed issue with website config menu lost (#9473) 2025-07-09 07:26:55 +00:00
ssongliu 0d548c7df9 fix: Fix http image registry edit exception (#9460) 2025-07-08 18:18:00 +08:00
ssongliu 8ec5a5bd4b fix: Fix menu internationalization issues (#9456) 2025-07-08 06:05:55 +00:00
ssongliu 2e8bcc09e6 fix: Fix dashboard refresh exception issue (#9455) 2025-07-08 04:15:53 +00:00
ssongliu 29a304e2c8 fix: Optimize container image repo restart detection logic (#9447) 2025-07-08 02:47:53 +00:00
ssongliu b1adc37920 fix: Modify OneDrive default connection settings (#9440) 2025-07-07 10:05:50 +00:00
CityFun 03fac3ec06 fix: Resolve the issue of lost GPU configuration after upgrade ollama (#9362) 2025-07-01 07:47:33 +00:00
蘭 6e465ac215 fix: Fix the issue of multiple selection moving folder errors (#9319)
refs: #8612
2025-06-27 06:03:24 +00:00
ssongliu 21bbadc37b fix: Resolve abnormal state after Docker restart (#9305) 2025-06-26 11:19:22 +00:00
ssongliu 3061d640ce fix: Modify the service status judgment logic (#9301) 2025-06-26 17:18:14 +08:00
ssongliu 874717cbcd fix: Remove code comments (#9297) 2025-06-26 06:39:22 +00:00
ssongliu 6850acabae fix: Optimize supervisor initialization logic (#9295) 2025-06-26 06:29:23 +00:00
ssongliu 890ea42749 fix: Fix ClamAV service name detection issue (#9292) 2025-06-26 14:27:57 +08:00
ssongliu 6aaf61cde4 fix: Increase systemctl operation timeout (#9281) 2025-06-25 08:21:22 +00:00
wanghe-fit2cloud 73930c5e29 feat: Compatible with system upgrades for v1.10.30 and earlier versions 2025-06-24 17:32:23 +08:00
ssongliu 8f8d280e90 Revert "fix: Fix the issue with upgrade failures (#9262)" (#9264)
This reverts commit b46192024f.
2025-06-24 17:24:42 +08:00
ssongliu b46192024f fix: Fix the issue with upgrade failures (#9262) 2025-06-24 08:51:13 +00:00
ssongliu 8f939ad5be fix: Make firewall operations compatible with OpenWrt (#9257) 2025-06-24 06:21:13 +00:00
ssongliu a6363ce4e5 fix: Fix the issue with cronjob report loading failure (#9254)
Refs #8684
2025-06-24 05:47:14 +00:00
ssongliu fd490b2642 fix: Modify the image repository validation rules (#9253)
Refs #8870
2025-06-24 05:45:13 +00:00
ssongliu 7624670ee0 fix: Add a restart prompt for HTTP-type mirror repositories (#9252)
Refs #8978
2025-06-24 05:43:14 +00:00
ssongliu 8c3145e1c6 fix: Fix the problem of ip changes in container upgrades for macvlan … (#9251)
Refs #8950
2025-06-24 05:41:13 +00:00
ssongliu bec7b6989f fix: Fix the issue of abnormal highlighting in the cronjob script (#9246)
Refs #8683
2025-06-23 10:40:14 +00:00
CityFun 326f2654e6 feat: add PbootCMS extensions template for php runtime (#9243) 2025-06-23 18:37:47 +08:00
CityFun 708499a335 feat: fix issue with build frontend failed (#8994) 2025-06-10 08:56:35 +00:00
CityFun bdeb842939 feat: change 1panel v1 docs url (#8993) 2025-06-10 08:16:34 +00:00
王贺 edefdce3ff docs: Update the installation script (#8979) 2025-06-10 11:03:13 +08:00
longjuan b348c36571 feat: add name attrs to login inputs for password managers to recognize (#8953) 2025-06-09 11:26:02 +08:00
玲珑芯ExquisiteCore 6ae150b64e feat: Move current status to first position in MySQL settings and set… (#8882)
* feat: Move current status to first position in MySQL and Redis settings and set as default tab
2025-06-03 10:22:00 +08:00
maninhill 3687b9bea2 chore: Update README.md 2025-05-25 16:42:01 +08:00
maninhill 711431da82 Update README.md 2025-05-25 16:40:02 +08:00
Alan e93afc33aa ini filetype support (#8628) 2025-05-14 16:52:41 +08:00
CityFun 82dd993751 fix: Fix issue with update website config (#8610) 2025-05-12 10:41:58 +00:00
Wankko Ree 9ba293e154 fix: the docker image which no tag but used be pruned (#8529) 2025-05-03 13:25:24 +08:00
igophper 8fa9f128d8 fix(cronjob): prevent unnecessary log updates and clean log formatting (#8481)
Refs #8425
2025-04-28 09:09:48 +08:00
maninhill ed9f1599e6 chore: Update README.md 2025-04-25 11:38:18 +08:00
igophper 787a29769d fix(editor): reset edit state after not saving content (#8474)
Refs #8461
2025-04-25 11:13:23 +08:00
巴山夜语andgcsong023 6b379389d3 WIP: refactor(service): Refactor OpenRC service manager (#8416)
* refactor(service): 重构 OpenRC 服务管理器

- 更新 IsEnabled 和 IsActive 检查逻辑,使用更可靠的命令
- 修复 ServiceExists 检查,直接使用文件路径判断
- 优化 FindServices 函数,扫描 /etc/init.d 目录
- 调整 BuildCommand 函数,支持 OpenRC 特定操作
- 修改 ParseStatus 函数,使用更新后的正则表达式

* feat(backend): 优化 Fail2ban 初始化配置以支持 Alpine 系统

- 增加对 Alpine 系统的特殊配置支持
- 改进防火墙类型检测逻辑,支持多种防火墙服务
- 增加 SSH 端口和认证日志路径的自动检测
- 优化配置文件模板,提高兼容性和安全性

* refactor(backend): 重构 SSH 日志解析功能

- 改进了对不同日志格式的支持,包括 secure, auth 和 messages 文件
- 优化了日志解析逻辑,提高了代码的可读性和可维护性
- 增加了对 RFC3339 时间格式的支持
- 改善了对失败登录尝试的解析,包括无效用户和连接关闭的情况
- 重构了日期解析和 IP 地址验证的逻辑

* refactor(upgrade): 优化升级服务中的初始化脚本选择逻辑

- 新增 selectInitScript 函数,根据系统初始化管理器类型选择合适的初始化脚本
- 支持 systemd、openrc 和 sysvinit 三种初始化管理器
- 对于 sysvinit,增加对 /etc/rc.common 文件存在性的判断,以区分不同的初始化脚本
- 默认情况下使用当前服务名称作为初始化脚本名称

* fix(upgrade): 修复升级时初始化脚本更新问题

- 修改了 criticalUpdates 数组中的服务脚本更新逻辑
- 在 selectInitScript 函数中增加了复制脚本文件的逻辑,以应对服务名和脚本名不一致的情况

* feat(snap): 添加初始化脚本到快照

- 在创建快照时,将服务脚本复制到 initscript 目录
- 然后将整个 initscript 目录复制到快照的目标目录
- 添加了日志输出,便于调试和记录

* refactor(backend): 重构快照恢复流程

- 移除了未使用的 import 语句
- 删除了注释掉的代码块
- 修改了 1Panel 服务恢复的逻辑,增加了对当前服务名称的获取
- 快照恢复过程中,根据宿主机类型,自动选择初始化服务脚本
- 添加了日志输出以提高可追踪性
- 优化了文件路径的处理方式

---------

Co-authored-by: gcsong023 <gcsong023@users.noreply.github.com>
2025-04-23 17:04:08 +08:00
ChengPlay 5bcbf32823 Fix issue with errors when reverse proxy source text includes comments. (#8444)
Refs https://github.com/1Panel-dev/1Panel/issues/8442
2025-04-22 07:51:06 +00:00
maninhill 0aa90e1b2d chore: Update README.md 2025-04-19 09:59:23 +08:00
maninhill a7b87211b4 chore: Update README.md 2025-04-19 09:53:46 +08:00
maninhill 2c666933a5 chore: Update README.md 2025-04-19 09:43:31 +08:00
maninhill f280b533c6 Update README.md 2025-04-19 09:42:05 +08:00
wanghe-fit2cloud 47a16bc9a9 chore(build): optimize build script 2025-04-18 14:48:50 +08:00
ChengPlay 713202b6eb feat: update golang.org/x/net version (#8412) 2025-04-17 03:02:20 +00:00
巴山夜语andgcsong023 79020abb1c feat(systemctl): implement service manager initialization and command execution (#8380)
* feat(systemctl): 实现服务管理器初始化和命令执行

- 新增 systemctl 包,实现对 systemd、openrc 和 sysvinit 三种服务管理器的支持
- 添加服务状态检查、启动、停止、重启和启用/禁用功能
- 实现服务发现和智能服务名处理
- 添加配置文件查看功能
- 优化错误处理和日志记录

* refactor(system): 重构系统服务管理逻辑

- 引入 systemctl 工具包以统一处理系统服务
- 优化服务状态获取、配置文件路径解析等逻辑
- 重构 HostToolService 中的 GetToolStatus 方法
- 更新 DockerService、SettingService 等相关服务的处理方式
- 调整快照创建和恢复过程中的服务处理逻辑

* feat(utils): 添加目录复制功能并优化文件复制逻辑

- 新增 CopyDirs 函数,用于复制整个目录及其内容
- 添加对符号链接的复制支持
- 实现通用的 Copy 函数,根据文件类型自动选择 CopyFile 或 CopyDirs
- 在 CopyFile 函数中增加对源文件是目录的检查和错误提示

* refactortoolbox: 重构 Fail2ban 和 Pure-FTPd 的管理逻辑

- 优化了 Fail2ban 和 Pure-FTPd 的启动、停止、重启等操作的实现
- 改进了 Fail2ban 版本信息的获取方法
- 统一了错误处理和日志记录的格式
- 调整了部分导入的包,提高了代码的可维护性

* build: 禁用 CGO 以提高构建性能和兼容性

- 在 Linux 后端构建命令中添加 CGO_ENABLED=0 环境变量
- 此修改可以提高构建速度,并确保生成的二进制文件在没有 C 库依赖的环境中也能运行

* refactor(docker): 重构 Docker 服务的重启和操作逻辑

- 添加 isDockerSnapInstalled 函数来判断 Docker 是否通过 Snap 安装
- 在 OperateDocker 和 restartDocker 函数中增加对 Snap 安装的处理
- 移除未使用的 getDockerRestartCommand 函数

* fix(service): 优化快照恢复后的服务重启逻辑

- 在使用 systemd 管理服务时,增加 daemon-reload 操作以确保服务配置更新
- 重启 1panel 服务,以应用快照恢复的更改

* refactor(server): 支持非 systemd 系统的恢复操作

- 增加 isSystemd 函数判断系统是否为 systemd 类型
- 根据系统类型选择性地恢复服务文件
- 兼容 systemd 和非 systemd 系统的恢复流程

* fix(upgrade): 优化升级过程中的服务重启逻辑

- 移动服务重启逻辑到版本号更新之后,修复因提前重启导致的版本号未更新BUG。
- 在 systemctl 重启之前添加 daemon-reload 命令

---------

Co-authored-by: gcsong023 <gcsong023@users.noreply.github.com>
2025-04-17 10:26:13 +08:00
chiichen 83ef41cf1a feat: basic support riscv64 (#8369)
* feat: add riscv64

* feat: correct arch related codes
2025-04-17 10:24:04 +08:00
dependabot[bot] 91675c69e9 chore(deps): bump golang.org/x/crypto from 0.32.0 to 0.35.0 (#8400)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.32.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.32.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.35.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 11:41:42 +08:00
ChengPlay 0e68065b40 feat: change commandPlaceHolder translate (#8391) 2025-04-14 08:56:12 +00:00
ssongliu 600b769083 fix: Fix the mysql root account permission switchover exception (#8378) 2025-04-11 07:08:02 +00:00
zhengkunwang e7670f96de feat: update mcp proxy_pass (#8374) 2025-04-10 16:45:24 +08:00
zhengkunwang 342c04b30a fix: fix issue with mcp bind website (#8356) 2025-04-09 03:55:56 +00:00
zhengkunwang 5ab8377ac1 feat: update allowPort check (#8355) 2025-04-09 02:47:54 +00:00
ssongliu 74eb2c8dd6 fix: Fix the problem that jumping to a directory that does not exist … (#8353) 2025-04-08 14:33:54 +00:00
zhengkunwang f136d97941 change mcp helper (#8352) 2025-04-08 10:59:53 +00:00
ssongliu a5629e3f42 fix: Fix the problem of failed MySQL permission modification (#8351) 2025-04-08 10:49:53 +00:00
zhengkunwang 515d6ca500 fix: fix bug with mcp-server status error (#8349) 2025-04-08 09:21:53 +00:00
zhengkunwang c0ccc0318d feat: add warn text for mcp server (#8346) 2025-04-08 05:55:52 +00:00
zhengkunwang 8196c11010 feat: import mcp server support env (#8344) 2025-04-08 03:11:52 +00:00
ssongliu 170bd22062 fix: Modify database verification rules (#8342) 2025-04-08 02:39:52 +00:00
zhengkunwang 097a33c68f style: update style for mcp server (#8338) 2025-04-07 11:09:51 +00:00
zhengkunwang 3be3bf9926 feat: remove env from mcp config (#8337) 2025-04-07 10:03:50 +00:00
zhengkunwang 038f2b4f54 feat: change mcp list style (#8336) 2025-04-07 09:35:50 +00:00
zhengkunwang f84e284c20 feat: change DNSTypes order (#8335) 2025-04-07 09:01:50 +00:00
ssongliu 48e75b5916 fix: Set bridge as the default network for container creation. (#8332) 2025-04-07 07:47:49 +00:00
zhengkunwang a12ceaee6e feat: add config for mcp server (#8331) 2025-04-07 07:41:51 +00:00
zhengkunwang 9050680115 feat: update mcp form label (#8327) 2025-04-07 02:49:49 +00:00
zhengkunwang 0022a9b508 fix: fix issue with install app no label (#8326) 2025-04-07 02:47:49 +00:00
zhengkunwang 3437d62f31 feat: Website SSL DNS Type support WestCN (#8305) 2025-04-02 10:21:54 +00:00
ssongliu dc880835d0 fix: Modify the container terminal connection mode (#8301) 2025-04-02 10:19:54 +00:00
ssongliu 1663bf8677 fix: Regenerates the interface document (#8298) 2025-04-02 09:07:54 +00:00
ssongliu aed236cbc9 fix: Ignore the scheduled task backup content failed to be uploaded (#8297)
Refs #7869
2025-04-02 09:05:54 +00:00
ssongliu 90be74504e feat: Support the retention of remote backup files when deleting sche… (#8296)
Refs #8236
2025-04-02 09:03:55 +00:00
zhengkunwang bba8aab18c feat: Add MCP management (#8299) 2025-04-02 09:01:54 +00:00
ssongliu 47d135ecca fix: Optimize database password verification and password change (#8294) 2025-04-02 16:03:44 +08:00
ssongliu d96fe49993 fix: Resolve cloudns reference error (#8293) 2025-04-02 03:41:53 +00:00
ssongliu ac396b61ce fix: Modify the panel password verification rule (#8291)
Refs #4290
2025-04-02 03:39:54 +00:00
ssongliu 6798d0c7ee fix: Fixed an update failure when the container network is bridge (#8290)
Refs #8209
2025-04-02 03:37:53 +00:00
亖 1490ed7541 dev:修复注释路由前缀 (#8292) 2025-04-02 10:10:00 +08:00
白铭骢 (Mingcong Bai) 48b3032de6 chore(deps): update modernc.org/sqlite to v1.29.7 (#8260)
With modernc.org/sqlite@v1.29.1, 1Panel fails to build on LoongArch with
the following error:

../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/defs.go:13:35: undefined: sqlite3_index_constraint
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/defs.go:14:35: undefined: sqlite3_index_orderby
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/defs.go:15:35: undefined: sqlite3_index_constraint_usage
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:19:5: undefined: Xsqlite3_threadsafe
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:29:11: undefined: Xsqlite3_config
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:29:32: undefined: SQLITE_CONFIG_MUTEX
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:29:122: undefined: SQLITE_OK
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:30:8: undefined: Xsqlite3_errstr
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:40:17: undefined: Sqlite3_mutex_methods
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:239:46: undefined: SQLITE_OK
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:239:46: too many errors

Upstream introduced LoongArch (loong64) target support in
modernc.org/sqlite@v1.29.7. Take a conservative approach and bump
modernc.org/sqlite to v1.29.7, this will allow for 1Panel to build on
LoongArch (both frontend and backend).

Signed-off-by: Mingcong Bai <jeffbai@aosc.io>
2025-03-27 13:45:10 +08:00
sunsky ed12bec460 feat: add DNS provider ClouDNS (#8207)
* feat: add DNS provider ClouDNS

* feat: ClouDNS account parameter validation
2025-03-25 10:12:14 +08:00
dependabot[bot] 3c4a1ea614 chore(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#8229)
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 17:38:30 +08:00
ssongliu 79eb0288b4 fix: Fix the problem that the terminal exit fails (#8189) 2025-03-19 03:43:21 +00:00
蘭 99775e1eb4 fix: Fix some issues (#8178) 2025-03-18 17:16:23 +08:00
zhengkunwang db848b41d2 feat: update buildx and containerd version (#8183) 2025-03-18 17:13:36 +08:00
david082321 b5b11dff86 Update tw.ts (#8161) 2025-03-17 10:58:19 +08:00
zhengkunwang 9cf4bfc606 feat: update golang.org/x/net version to v0.37.0 (#8146) 2025-03-13 18:30:25 +08:00
ThinkStu 221162d44f Update zh.ts (#8136)
correct typos
2025-03-12 22:43:20 +08:00
蘭 617970a1f8 fix: license setting data (#8132) 2025-03-12 16:25:32 +08:00
156 changed files with 7426 additions and 2826 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20.2' node-version: '22.19.0'
- name: Build Web - name: Build Web
run: | run: |
cd frontend && npm install && npm run build:pro cd frontend && npm install && npm run build:pro
@@ -21,7 +21,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.24'
- name: Build Release - name: Build Release
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
with: with:
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20.2' node-version: '22.19.0'
- name: Build Web - name: Build Web
run: | run: |
cd frontend && npm install && npm run build:pro cd frontend && npm install && npm run build:pro
@@ -21,7 +21,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.24'
- name: Build Release - name: Build Release
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
with: with:
+5 -3
View File
@@ -10,7 +10,7 @@ before:
- ./ci/script.sh - ./ci/script.sh
- sed -i 's@ORIGINAL_VERSION=.*@ORIGINAL_VERSION=v{{ .Version }}@g' 1pctl - sed -i 's@ORIGINAL_VERSION=.*@ORIGINAL_VERSION=v{{ .Version }}@g' 1pctl
- go mod tidy - go mod tidy
builds: builds:
- main: ./cmd/server/main.go - main: ./cmd/server/main.go
binary: 1panel binary: 1panel
@@ -31,6 +31,7 @@ builds:
- arm - arm
- ppc64le - ppc64le
- s390x - s390x
- riscv64
archives: archives:
- formats: [ 'tar.gz' ] - formats: [ 'tar.gz' ]
@@ -38,10 +39,11 @@ archives:
wrap_in_directory: true wrap_in_directory: true
files: files:
- 1pctl - 1pctl
- 1panel.service
- install.sh - install.sh
- GeoIP.mmdb - 1panel.service
- initscript/*
- lang/* - lang/*
- GeoIP.mmdb
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
+2 -2
View File
@@ -23,7 +23,7 @@ build_frontend:
build_backend_on_linux: build_backend_on_linux:
cd $(SERVER_PATH) \ cd $(SERVER_PATH) \
&& GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
build_backend_on_darwin: build_backend_on_darwin:
cd $(SERVER_PATH) \ cd $(SERVER_PATH) \
@@ -31,4 +31,4 @@ build_backend_on_darwin:
build_all: build_frontend build_backend_on_linux build_all: build_frontend build_backend_on_linux
build_on_local: clean_assets build_frontend build_backend_on_darwin upx_bin build_on_local: clean_assets build_frontend build_backend_on_darwin
+12 -3
View File
@@ -1,5 +1,5 @@
<p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p> <p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p>
<p align="center"><b>Top-Rated Web-based Linux Server Management Tool</b><br>Best VPS control panel<br>新一代的 Linux 服务器运维管理面板</p> <p align="center"><b>Simplify Linux Server Management</b></p>
<p align="center"> <p align="center">
<a href="https://trendshift.io/repositories/2462" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2462" alt="1Panel-dev%2F1Panel | Trendshift" style="width: 240px; height: auto;" /></a> <a href="https://trendshift.io/repositories/2462" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2462" alt="1Panel-dev%2F1Panel | Trendshift" style="width: 240px; height: auto;" /></a>
</p> </p>
@@ -31,13 +31,14 @@
------------------------------ ------------------------------
1Panel is an open-source, modern web-based control panel for Linux server management. 1Panel provides an intuitive web interface and MCP Server to manage websites, files, containers, databases, and LLMs on a Linux server.
- **Efficient Management**: Through a user-friendly web graphical interface, 1Panel enables users to effortlessly manage their Linux servers. Key features include host monitoring, file management, database administration, container management, LLMs management. - **Efficient Management**: Through a user-friendly web graphical interface, 1Panel enables users to effortlessly manage their Linux servers. Key features include host monitoring, file management, database administration, container management, LLMs management.
- **Rapid Website Deployment**: With deep integration of the popular open-source website building software WordPress, 1Panel streamlines the process of domain binding and SSL certificate configuration, all achievable with just one click. - **Rapid Website Deployment**: With deep integration of the popular open-source website building software WordPress, 1Panel streamlines the process of domain binding and SSL certificate configuration, all achievable with just one click.
- **Application Store**: 1Panel curates a wide range of high-quality open-source tools and applications, facilitating easy installation and updates for its users. - **Application Store**: 1Panel curates a wide range of high-quality open-source tools and applications, facilitating easy installation and updates for its users.
- **Security and Reliability**: By leveraging containerization and secure application deployment practices, 1Panel minimizes vulnerability exposure. It further enhances security through integrated firewall management and log auditing capabilities. - **Security and Reliability**: By leveraging containerization and secure application deployment practices, 1Panel minimizes vulnerability exposure. It further enhances security through integrated firewall management and log auditing capabilities.
- **One-Click Backup & Restore**: Data protection is made simple with 1Panel's one-click backup and restore functionality, supporting various cloud storage solutions to ensure data integrity and availability. - **One-Click Backup & Restore**: Data protection is made simple with 1Panel's one-click backup and restore functionality, supporting various cloud storage solutions to ensure data integrity and availability.
- **MCP Server**: [1Panel MCP Server](https://github.com/1Panel-dev/mcp-1panel) allow user to execute server operations via natural language.
## Quick Start ## Quick Start
@@ -49,7 +50,7 @@ curl -sSL https://resource.1panel.pro/quick_start.sh -o quick_start.sh && bash q
Please refer to our [documentation](https://docs.1panel.pro/quick_start/) for more details. Please refer to our [documentation](https://docs.1panel.pro/quick_start/) for more details.
中国用户请使用这个 [安装脚本](https://1panel.cn/docs/installation/online_installation/),其应用数量比国际版本更丰富。 中国用户请使用此 [安装脚本](https://1panel.cn/docs/v2/installation/online_installation/),默认安装 V2 版本,应用数量相较国际版本更丰富。
## Screenshot ## Screenshot
@@ -69,6 +70,10 @@ Compared to the OSS Edition, 1Panel Pro Edition provides users with a wealth of
If you discover any security issues, please refer to [SECURITY.md](/SECURITY.md). If you discover any security issues, please refer to [SECURITY.md](/SECURITY.md).
## GitAds Sponsored
[![Sponsored by GitAds](https://gitads.dev/v1/ad-serve?source=1panel-dev/1panel@github)](https://gitads.dev/v1/ad-track?source=1panel-dev/1panel@github)
## License ## License
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
@@ -76,3 +81,7 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
<https://www.gnu.org/licenses/gpl-3.0.html> <https://www.gnu.org/licenses/gpl-3.0.html>
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
<!-- GitAds-Verify: 9M8AMBB11GYJ3RUNXGB5R1MR71KPATU9 -->
+17 -6
View File
@@ -2,12 +2,14 @@ package v1
import ( import (
"encoding/base64" "encoding/base64"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/captcha" "github.com/1Panel-dev/1Panel/backend/utils/captcha"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -26,7 +28,9 @@ func (b *BaseApi) Login(c *gin.Context) {
return return
} }
if req.AuthMethod != "jwt" && !req.IgnoreCaptcha { ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
if needCaptcha {
if err := captcha.VerifyCode(req.CaptchaID, req.Captcha); err != nil { if err := captcha.VerifyCode(req.CaptchaID, req.Captcha); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
@@ -48,9 +52,11 @@ func (b *BaseApi) Login(c *gin.Context) {
user, err := authService.Login(c, req, string(entrance)) user, err := authService.Login(c, req, string(entrance))
go saveLoginLogs(c, err) go saveLoginLogs(c, err)
if err != nil { if err != nil {
global.IPTracker.SetNeedCaptcha(ip)
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }
global.IPTracker.Clear(ip)
helper.SuccessWithData(c, user) helper.SuccessWithData(c, user)
} }
@@ -134,16 +140,21 @@ func (b *BaseApi) CheckIsIntl(c *gin.Context) {
} }
// @Tags Auth // @Tags Auth
// @Summary Load System Language // @Summary Load System Setting for login
// @Success 200 {string} language // @Success 200 {object} dto.LoginSetting
// @Router /auth/language [get] // @Router /auth/setting [get]
func (b *BaseApi) GetLanguage(c *gin.Context) { func (b *BaseApi) GetAuthSetting(c *gin.Context) {
settingInfo, err := settingService.GetSettingInfo() settingInfo, err := settingService.GetSettingInfo()
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }
helper.SuccessWithData(c, settingInfo.Language) ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
helper.SuccessWithData(c, dto.LoginSetting{
NeedCaptcha: needCaptcha,
Language: settingInfo.Language,
})
} }
func saveLoginLogs(c *gin.Context, err error) { func saveLoginLogs(c *gin.Context, err error) {
+2
View File
@@ -68,4 +68,6 @@ var (
favoriteService = service.NewIFavoriteService() favoriteService = service.NewIFavoriteService()
websiteCAService = service.NewIWebsiteCAService() websiteCAService = service.NewIWebsiteCAService()
mcpServerService = service.NewIMcpServerService()
) )
+12 -3
View File
@@ -410,11 +410,20 @@ func (b *BaseApi) CheckFile(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if _, err := os.Stat(req.Path); err != nil { fileOp := files.NewFileOp()
helper.SuccessWithData(c, false) if fileOp.Stat(req.Path) {
helper.SuccessWithData(c, true)
return return
} }
helper.SuccessWithData(c, true) if req.WithInit {
if err := fileOp.CreateDir(req.Path, 0644); err != nil {
helper.SuccessWithData(c, false)
return
}
helper.SuccessWithData(c, true)
return
}
helper.SuccessWithData(c, false)
} }
// @Tags File // @Tags File
+8 -8
View File
@@ -14,7 +14,7 @@ import (
// @Success 200 {object} response.HostToolRes // @Success 200 {object} response.HostToolRes
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/tool [post] // @Router /hosts/tool [post]
func (b *BaseApi) GetToolStatus(c *gin.Context) { func (b *BaseApi) GetToolStatus(c *gin.Context) {
var req request.HostToolReq var req request.HostToolReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -36,7 +36,7 @@ func (b *BaseApi) GetToolStatus(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/tool/create [post] // @Router /hosts/tool/create [post]
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 [type] 配置","formatEN":"create [type] config"} // @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 [type] 配置","formatEN":"create [type] config"}
func (b *BaseApi) InitToolConfig(c *gin.Context) { func (b *BaseApi) InitToolConfig(c *gin.Context) {
var req request.HostToolCreate var req request.HostToolCreate
@@ -58,7 +58,7 @@ func (b *BaseApi) InitToolConfig(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/tool/operate [post] // @Router /hosts/tool/operate [post]
// @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"} // @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"}
func (b *BaseApi) OperateTool(c *gin.Context) { func (b *BaseApi) OperateTool(c *gin.Context) {
var req request.HostToolReq var req request.HostToolReq
@@ -80,7 +80,7 @@ func (b *BaseApi) OperateTool(c *gin.Context) {
// @Success 200 {object} response.HostToolConfig // @Success 200 {object} response.HostToolConfig
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/tool/config [post] // @Router /hosts/tool/config [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机工具配置文件 ","formatEN":"[operate] tool config"} // @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机工具配置文件 ","formatEN":"[operate] tool config"}
func (b *BaseApi) OperateToolConfig(c *gin.Context) { func (b *BaseApi) OperateToolConfig(c *gin.Context) {
var req request.HostToolConfig var req request.HostToolConfig
@@ -103,7 +103,7 @@ func (b *BaseApi) OperateToolConfig(c *gin.Context) {
// @Success 200 {string} logContent // @Success 200 {string} logContent
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/tool/log [post] // @Router /hosts/tool/log [post]
func (b *BaseApi) GetToolLog(c *gin.Context) { func (b *BaseApi) GetToolLog(c *gin.Context) {
var req request.HostToolLogReq var req request.HostToolLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -125,7 +125,7 @@ func (b *BaseApi) GetToolLog(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/tool/supervisor/process [post] // @Router /hosts/tool/supervisor/process [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 守护进程 ","formatEN":"[operate] process"} // @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 守护进程 ","formatEN":"[operate] process"}
func (b *BaseApi) OperateProcess(c *gin.Context) { func (b *BaseApi) OperateProcess(c *gin.Context) {
var req request.SupervisorProcessConfig var req request.SupervisorProcessConfig
@@ -147,7 +147,7 @@ func (b *BaseApi) OperateProcess(c *gin.Context) {
// @Success 200 {object} response.SupervisorProcessConfig // @Success 200 {object} response.SupervisorProcessConfig
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/tool/supervisor/process [get] // @Router /hosts/tool/supervisor/process [get]
func (b *BaseApi) GetProcess(c *gin.Context) { func (b *BaseApi) GetProcess(c *gin.Context) {
configs, err := hostToolService.GetSupervisorProcessConfig() configs, err := hostToolService.GetSupervisorProcessConfig()
if err != nil { if err != nil {
@@ -164,7 +164,7 @@ func (b *BaseApi) GetProcess(c *gin.Context) {
// @Success 200 {string} content // @Success 200 {string} content
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/tool/supervisor/process/file [post] // @Router /hosts/tool/supervisor/process/file [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"} // @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"}
func (b *BaseApi) GetProcessFile(c *gin.Context) { func (b *BaseApi) GetProcessFile(c *gin.Context) {
var req request.SupervisorProcessFileReq var req request.SupervisorProcessFileReq
+5 -5
View File
@@ -76,7 +76,7 @@ func (b *BaseApi) CheckRepoStatus(c *gin.Context) {
// @Tags Container Image-repo // @Tags Container Image-repo
// @Summary Create image repo // @Summary Create image repo
// @Accept json // @Accept json
// @Param request body dto.ImageRepoDelete true "request" // @Param request body dto.ImageRepoCreate true "request"
// @Produce json // @Produce json
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
@@ -99,20 +99,20 @@ func (b *BaseApi) CreateRepo(c *gin.Context) {
// @Tags Container Image-repo // @Tags Container Image-repo
// @Summary Delete image repo // @Summary Delete image repo
// @Accept json // @Accept json
// @Param request body dto.ImageRepoDelete true "request" // @Param request body dto.OperateByID true "request"
// @Produce json // @Produce json
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /containers/repo/del [post] // @Router /containers/repo/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"image_repos","output_column":"name","output_value":"names"}],"formatZH":"删除镜像仓库 [names]","formatEN":"delete image repo [names]"} // @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"image_repos","output_column":"name","output_value":"name"}],"formatZH":"删除镜像仓库 [name]","formatEN":"delete image repo [name]"}
func (b *BaseApi) DeleteRepo(c *gin.Context) { func (b *BaseApi) DeleteRepo(c *gin.Context) {
var req dto.ImageRepoDelete var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if err := imageRepoService.BatchDelete(req); err != nil { if err := imageRepoService.Delete(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }
+167
View File
@@ -0,0 +1,167 @@
package v1
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
)
// @Tags McpServer
// @Summary List mcp servers
// @Accept json
// @Param request body request.McpServerSearch true "request"
// @Success 200 {object} response.McpServersRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/search [post]
func (b *BaseApi) PageMcpServers(c *gin.Context) {
var req request.McpServerSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list := mcpServerService.Page(req)
helper.SuccessWithData(c, list)
}
// @Tags McpServer
// @Summary Create mcp server
// @Accept json
// @Param request body request.McpServerCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server [post]
func (b *BaseApi) CreateMcpServer(c *gin.Context) {
var req request.McpServerCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Create(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Update mcp server
// @Accept json
// @Param request body request.McpServerUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server/update [post]
func (b *BaseApi) UpdateMcpServer(c *gin.Context) {
var req request.McpServerUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Update(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Delete mcp server
// @Accept json
// @Param request body request.McpServerDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server/del [post]
func (b *BaseApi) DeleteMcpServer(c *gin.Context) {
var req request.McpServerDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Delete(req.ID)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Operate mcp server
// @Accept json
// @Param request body request.McpServerOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server/op [post]
func (b *BaseApi) OperateMcpServer(c *gin.Context) {
var req request.McpServerOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Operate(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Bind Domain for mcp server
// @Accept json
// @Param request body request.McpBindDomain true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/domain/bind [post]
func (b *BaseApi) BindMcpDomain(c *gin.Context) {
var req request.McpBindDomain
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.BindDomain(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Update bind Domain for mcp server
// @Accept json
// @Param request body request.McpBindDomainUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/domain/update [post]
func (b *BaseApi) UpdateMcpBindDomain(c *gin.Context) {
var req request.McpBindDomainUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.UpdateBindDomain(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Get bin Domain for mcp server
// @Accept json
// @Success 200 {object} response.McpBindDomainRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/domain/get [get]
func (b *BaseApi) GetMcpBindDomain(c *gin.Context) {
res, err := mcpServerService.GetBindDomain()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}
+8 -8
View File
@@ -12,7 +12,7 @@ import (
// @Success 200 {object} dto.SSHInfo // @Success 200 {object} dto.SSHInfo
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/ssh/search [post] // @Router /hosts/ssh/search [post]
func (b *BaseApi) GetSSHInfo(c *gin.Context) { func (b *BaseApi) GetSSHInfo(c *gin.Context) {
info, err := sshService.GetSSHInfo() info, err := sshService.GetSSHInfo()
if err != nil { if err != nil {
@@ -29,7 +29,7 @@ func (b *BaseApi) GetSSHInfo(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/ssh/operate [post] // @Router /hosts/ssh/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] SSH ","formatEN":"[operation] SSH"} // @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] SSH ","formatEN":"[operation] SSH"}
func (b *BaseApi) OperateSSH(c *gin.Context) { func (b *BaseApi) OperateSSH(c *gin.Context) {
var req dto.Operate var req dto.Operate
@@ -51,7 +51,7 @@ func (b *BaseApi) OperateSSH(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/ssh/update [post] // @Router /hosts/ssh/update [post]
// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置 [key] => [value]","formatEN":"update SSH setting [key] => [value]"} // @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置 [key] => [value]","formatEN":"update SSH setting [key] => [value]"}
func (b *BaseApi) UpdateSSH(c *gin.Context) { func (b *BaseApi) UpdateSSH(c *gin.Context) {
var req dto.SSHUpdate var req dto.SSHUpdate
@@ -73,7 +73,7 @@ func (b *BaseApi) UpdateSSH(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/conffile/update [post] // @Router /hosts/conffile/update [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件","formatEN":"update SSH conf"} // @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件","formatEN":"update SSH conf"}
func (b *BaseApi) UpdateSSHByfile(c *gin.Context) { func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
var req dto.SSHConf var req dto.SSHConf
@@ -95,7 +95,7 @@ func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/ssh/generate [post] // @Router /hosts/ssh/generate [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"} // @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"}
func (b *BaseApi) GenerateSSH(c *gin.Context) { func (b *BaseApi) GenerateSSH(c *gin.Context) {
var req dto.GenerateSSH var req dto.GenerateSSH
@@ -117,7 +117,7 @@ func (b *BaseApi) GenerateSSH(c *gin.Context) {
// @Success 200 {string} data // @Success 200 {string} data
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/ssh/secret [post] // @Router /hosts/ssh/secret [post]
func (b *BaseApi) LoadSSHSecret(c *gin.Context) { func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
var req dto.GenerateLoad var req dto.GenerateLoad
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -139,7 +139,7 @@ func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
// @Success 200 {object} dto.SSHLog // @Success 200 {object} dto.SSHLog
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/ssh/log [post] // @Router /hosts/ssh/log [post]
func (b *BaseApi) LoadSSHLogs(c *gin.Context) { func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
var req dto.SearchSSHLog var req dto.SearchSSHLog
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -159,7 +159,7 @@ func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
// @Success 200 {string} data // @Success 200 {string} data
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /host/ssh/conf [get] // @Router /hosts/ssh/conf [get]
func (b *BaseApi) LoadSSHConf(c *gin.Context) { func (b *BaseApi) LoadSSHConf(c *gin.Context) {
data, err := sshService.LoadSSHConf() data, err := sshService.LoadSSHConf()
if err != nil { if err != nil {
+29 -29
View File
@@ -68,9 +68,8 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
<-quitChan <-quitChan
if wshandleError(wsConn, err) { dt := time.Now().Add(time.Second)
return _ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
}
} }
func (b *BaseApi) ContainerWsSSH(c *gin.Context) { func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
@@ -97,7 +96,7 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
} }
source := c.Query("source") source := c.Query("source")
var containerID string var containerID string
var initCmd string var initCmd []string
switch source { switch source {
case "redis": case "redis":
containerID, initCmd, err = loadRedisInitCmd(c) containerID, initCmd, err = loadRedisInitCmd(c)
@@ -114,11 +113,11 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
return return
} }
pidMap := loadMapFromDockerTop(containerID) pidMap := loadMapFromDockerTop(containerID)
slave, err := terminal.NewCommand("clear && " + initCmd) slave, err := terminal.NewCommand(initCmd)
if wshandleError(wsConn, err) { if wshandleError(wsConn, err) {
return return
} }
defer killBash(containerID, strings.ReplaceAll(initCmd, fmt.Sprintf("docker exec -it %s ", containerID), ""), pidMap) defer killBash(containerID, strings.ReplaceAll(strings.Join(initCmd, " "), fmt.Sprintf("exec -it %s ", containerID), ""), pidMap)
defer slave.Close() defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false) tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
@@ -133,67 +132,68 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
<-quitChan <-quitChan
global.LOG.Info("websocket finished") global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) { dt := time.Now().Add(time.Second)
return _ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
}
} }
func loadRedisInitCmd(c *gin.Context) (string, string, error) { func loadRedisInitCmd(c *gin.Context) (string, []string, error) {
name := c.Query("name") name := c.Query("name")
from := c.Query("from") from := c.Query("from")
commands := "redis-cli" commands := []string{"exec", "-it"}
database, err := databaseService.Get(name) database, err := databaseService.Get(name)
if err != nil { if err != nil {
return "", "", fmt.Errorf("no such database in db, err: %v", err) return "", nil, fmt.Errorf("no such database in db, err: %v", err)
} }
if from == "local" { if from == "local" {
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"}) redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
if err != nil { if err != nil {
return "", "", fmt.Errorf("no such app in db, err: %v", err) return "", nil, fmt.Errorf("no such app in db, err: %v", err)
} }
name = redisInfo.ContainerName name = redisInfo.ContainerName
commands = append(commands, []string{name, "redis-cli"}...)
if len(database.Password) != 0 { if len(database.Password) != 0 {
commands = "redis-cli -a " + database.Password + " --no-auth-warning" commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
} }
} else { } else {
commands = fmt.Sprintf("redis-cli -h %s -p %v", database.Address, database.Port)
if len(database.Password) != 0 {
commands = fmt.Sprintf("redis-cli -h %s -p %v -a %s --no-auth-warning", database.Address, database.Port, database.Password)
}
name = "1Panel-redis-cli-tools" name = "1Panel-redis-cli-tools"
commands = append(commands, []string{name, "redis-cli", "-h", database.Address, "-p", fmt.Sprintf("%v", database.Port)}...)
if len(database.Password) != 0 {
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
}
} }
return name, fmt.Sprintf("docker exec -it %s %s", name, commands), nil return name, commands, nil
} }
func loadOllamaInitCmd(c *gin.Context) (string, string, error) { func loadOllamaInitCmd(c *gin.Context) (string, []string, error) {
name := c.Query("name") name := c.Query("name")
if cmd.CheckIllegal(name) { if cmd.CheckIllegal(name) {
return "", "", fmt.Errorf("ollama model %s contains illegal characters", name) return "", nil, fmt.Errorf("ollama model %s contains illegal characters", name)
} }
ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"}) ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
if err != nil { if err != nil {
return "", "", fmt.Errorf("no such app in db, err: %v", err) return "", nil, fmt.Errorf("no such app in db, err: %v", err)
} }
containerName := ollamaInfo.ContainerName containerName := ollamaInfo.ContainerName
return containerName, fmt.Sprintf("docker exec -it %s ollama run %s", containerName, name), nil return containerName, []string{"exec", "-it", containerName, "ollama", "run", name}, nil
} }
func loadContainerInitCmd(c *gin.Context) (string, string, error) { func loadContainerInitCmd(c *gin.Context) (string, []string, error) {
containerID := c.Query("containerid") containerID := c.Query("containerid")
command := c.Query("command") command := c.Query("command")
user := c.Query("user") user := c.Query("user")
if cmd.CheckIllegal(user, containerID, command) { if cmd.CheckIllegal(user, containerID, command) {
return "", "", fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID) return "", nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
} }
if len(command) == 0 || len(containerID) == 0 { if len(command) == 0 || len(containerID) == 0 {
return "", "", fmt.Errorf("error param of command: %s or containerID: %s", command, containerID) return "", nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
} }
command = fmt.Sprintf("docker exec -it %s %s", containerID, command) commands := []string{"exec", "-it", containerID, command}
if len(user) != 0 { if len(user) != 0 {
command = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command) commands = []string{"exec", "-it", "-u", user, containerID, command}
} }
return containerID, command, nil return containerID, commands, nil
} }
func wshandleError(ws *websocket.Conn, err error) bool { func wshandleError(ws *websocket.Conn, err error) bool {
+11 -7
View File
@@ -23,13 +23,12 @@ type MfaCredential struct {
} }
type Login struct { type Login struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
IgnoreCaptcha bool `json:"ignoreCaptcha"` Captcha string `json:"captcha"`
Captcha string `json:"captcha"` CaptchaID string `json:"captchaID"`
CaptchaID string `json:"captchaID"` AuthMethod string `json:"authMethod" validate:"required,oneof=jwt session"`
AuthMethod string `json:"authMethod" validate:"required,oneof=jwt session"` Language string `json:"language" validate:"required,oneof=zh en tw ja ko ru ms 'pt-BR'"`
Language string `json:"language" validate:"required,oneof=zh en tw ja ko ru ms 'pt-BR'"`
} }
type MFALogin struct { type MFALogin struct {
@@ -38,3 +37,8 @@ type MFALogin struct {
Code string `json:"code" validate:"required"` Code string `json:"code" validate:"required"`
AuthMethod string `json:"authMethod"` AuthMethod string `json:"authMethod"`
} }
type LoginSetting struct {
NeedCaptcha bool `json:"needCaptcha"`
Language string `json:"language"`
}
+7 -5
View File
@@ -71,14 +71,16 @@ type CronjobDownload struct {
} }
type CronjobClean struct { type CronjobClean struct {
IsDelete bool `json:"isDelete"` IsDelete bool `json:"isDelete"`
CleanData bool `json:"cleanData"` CleanData bool `json:"cleanData"`
CronjobID uint `json:"cronjobID" validate:"required"` CleanRemoteData bool `json:"cleanRemoteData"`
CronjobID uint `json:"cronjobID" validate:"required"`
} }
type CronjobBatchDelete struct { type CronjobBatchDelete struct {
CleanData bool `json:"cleanData"` CleanData bool `json:"cleanData"`
IDs []uint `json:"ids" validate:"required"` CleanRemoteData bool `json:"cleanRemoteData"`
IDs []uint `json:"ids" validate:"required"`
} }
type CronjobInfo struct { type CronjobInfo struct {
+4 -4
View File
@@ -6,8 +6,8 @@ type ImageRepoCreate struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
DownloadUrl string `json:"downloadUrl"` DownloadUrl string `json:"downloadUrl"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Username string `json:"username" validate:"max=256"` Username string `json:"username"`
Password string `json:"password" validate:"max=256"` Password string `json:"password"`
Auth bool `json:"auth"` Auth bool `json:"auth"`
} }
@@ -15,8 +15,8 @@ type ImageRepoUpdate struct {
ID uint `json:"id"` ID uint `json:"id"`
DownloadUrl string `json:"downloadUrl"` DownloadUrl string `json:"downloadUrl"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Username string `json:"username" validate:"max=256"` Username string `json:"username"`
Password string `json:"password" validate:"max=256"` Password string `json:"password"`
Auth bool `json:"auth"` Auth bool `json:"auth"`
} }
+10
View File
@@ -0,0 +1,10 @@
package dto
type DockerComposeService struct {
Image string `yaml:"image"`
ContainerName string `yaml:"container_name"`
Restart string `yaml:"restart"`
Ports []string `yaml:"ports"`
Environment []string `yaml:"environment"`
Command []string `yaml:"command"`
}
+8 -6
View File
@@ -76,7 +76,8 @@ type FileRename struct {
} }
type FilePathCheck struct { type FilePathCheck struct {
Path string `json:"path" validate:"required"` Path string `json:"path" validate:"required"`
WithInit bool `json:"withInit"`
} }
type FilePathsCheck struct { type FilePathsCheck struct {
@@ -91,11 +92,12 @@ type FileWget struct {
} }
type FileMove struct { type FileMove struct {
Type string `json:"type" validate:"required"` Type string `json:"type" validate:"required"`
OldPaths []string `json:"oldPaths" validate:"required"` OldPaths []string `json:"oldPaths" validate:"required"`
NewPath string `json:"newPath" validate:"required"` NewPath string `json:"newPath" validate:"required"`
Name string `json:"name"` Name string `json:"name"`
Cover bool `json:"cover"` Cover bool `json:"cover"`
CoverPaths []string `json:"coverPaths"`
} }
type FileDownload struct { type FileDownload struct {
+57
View File
@@ -0,0 +1,57 @@
package request
import "github.com/1Panel-dev/1Panel/backend/app/dto"
type McpServerSearch struct {
dto.PageInfo
Name string `json:"name"`
Sync bool `json:"sync"`
}
type McpServerCreate struct {
Name string `json:"name" validate:"required"`
Command string `json:"command" validate:"required"`
Environments []Environment `json:"environments"`
Volumes []Volume `json:"volumes"`
Port int `json:"port" validate:"required"`
ContainerName string `json:"containerName"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
HostIP string `json:"hostIP"`
}
type McpServerUpdate struct {
ID uint `json:"id" validate:"required"`
McpServerCreate
}
type Environment struct {
Key string `json:"key"`
Value string `json:"value"`
}
type Volume struct {
Source string `json:"source"`
Target string `json:"target"`
}
type McpServerDelete struct {
ID uint `json:"id" validate:"required"`
}
type McpServerOperate struct {
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
}
type McpBindDomain struct {
Domain string `json:"domain" validate:"required"`
SSLID uint `json:"sslID"`
IPList string `json:"ipList"`
}
type McpBindDomainUpdate struct {
WebsiteID uint `json:"websiteID" validate:"required"`
SSLID uint `json:"sslID"`
IPList string `json:"ipList"`
}
+1
View File
@@ -53,4 +53,5 @@ type ExistFileInfo struct {
Path string `json:"path"` Path string `json:"path"`
Size float64 `json:"size"` Size float64 `json:"size"`
ModTime time.Time `json:"modTime"` ModTime time.Time `json:"modTime"`
IsDir bool `json:"isDir"`
} }
+26
View File
@@ -0,0 +1,26 @@
package response
import (
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/model"
)
type McpServersRes struct {
Items []McpServerDTO `json:"items"`
Total int64 `json:"total"`
}
type McpServerDTO struct {
model.McpServer
Environments []request.Environment `json:"environments"`
Volumes []request.Volume `json:"volumes"`
}
type McpBindDomainRes struct {
Domain string `json:"domain"`
SSLID uint `json:"sslID"`
AcmeAccountID uint `json:"acmeAccountID"`
AllowIPs []string `json:"allowIPs"`
WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"`
}
+18
View File
@@ -0,0 +1,18 @@
package model
type McpServer struct {
BaseModel
Name string `json:"name"`
DockerCompose string `json:"dockerCompose"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
Message string `json:"message"`
Port int `json:"port"`
Status string `json:"status"`
Env string `json:"env"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
WebsiteID int `json:"websiteID"`
Dir string `json:"dir"`
HostIP string `json:"hostIP"`
}
+7
View File
@@ -16,6 +16,7 @@ type IGroupRepo interface {
Delete(opts ...DBOption) error Delete(opts ...DBOption) error
CancelDefault(groupType string) error CancelDefault(groupType string) error
WithByHostDefault() DBOption WithByHostDefault() DBOption
WithByWebsiteDefault() DBOption
} }
func NewIGroupRepo() IGroupRepo { func NewIGroupRepo() IGroupRepo {
@@ -56,6 +57,12 @@ func (u *GroupRepo) WithByHostDefault() DBOption {
} }
} }
func (u *GroupRepo) WithByWebsiteDefault() DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("is_default = ? AND type = ?", 1, "website")
}
}
func (u *GroupRepo) Delete(opts ...DBOption) error { func (u *GroupRepo) Delete(opts ...DBOption) error {
db := global.DB db := global.DB
for _, opt := range opts { for _, opt := range opts {
+56
View File
@@ -0,0 +1,56 @@
package repo
import "github.com/1Panel-dev/1Panel/backend/app/model"
type McpServerRepo struct {
}
type IMcpServerRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error)
GetFirst(opts ...DBOption) (*model.McpServer, error)
Create(mcpServer *model.McpServer) error
Save(mcpServer *model.McpServer) error
DeleteBy(opts ...DBOption) error
List(opts ...DBOption) ([]model.McpServer, error)
}
func NewIMcpServerRepo() IMcpServerRepo {
return &McpServerRepo{}
}
func (m McpServerRepo) Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error) {
var servers []model.McpServer
db := getDb(opts...).Model(&model.McpServer{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&servers).Error
return count, servers, err
}
func (m McpServerRepo) GetFirst(opts ...DBOption) (*model.McpServer, error) {
var mcpServer model.McpServer
if err := getDb(opts...).First(&mcpServer).Error; err != nil {
return nil, err
}
return &mcpServer, nil
}
func (m McpServerRepo) List(opts ...DBOption) ([]model.McpServer, error) {
var mcpServers []model.McpServer
if err := getDb(opts...).Find(&mcpServers).Error; err != nil {
return nil, err
}
return mcpServers, nil
}
func (m McpServerRepo) Create(mcpServer *model.McpServer) error {
return getDb().Create(mcpServer).Error
}
func (m McpServerRepo) Save(mcpServer *model.McpServer) error {
return getDb().Save(mcpServer).Error
}
func (m McpServerRepo) DeleteBy(opts ...DBOption) error {
return getDb(opts...).Delete(&model.McpServer{}).Error
}
+3
View File
@@ -839,6 +839,9 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
"param": strings.ReplaceAll(appInstall.Param, oldVal, newVal), "param": strings.ReplaceAll(appInstall.Param, oldVal, newVal),
"env": strings.ReplaceAll(appInstall.Env, oldVal, newVal), "env": strings.ReplaceAll(appInstall.Env, oldVal, newVal),
}, commonRepo.WithByID(appInstall.ID)) }, commonRepo.WithByID(appInstall.ID))
if appKey == "mysql" || appKey == "postgresql" {
return nil
}
} }
if param == "user-password" { if param == "user-password" {
oldVal = fmt.Sprintf("\"PANEL_DB_USER_PASSWORD\":\"%v\"", appInstall.UserPassword) oldVal = fmt.Sprintf("\"PANEL_DB_USER_PASSWORD\":\"%v\"", appInstall.UserPassword)
+31 -6
View File
@@ -442,17 +442,14 @@ func deleteLink(ctx context.Context, install *model.AppInstall, deleteDB bool, f
return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID)) return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID))
} }
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) { func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map[string]interface{}, error) {
if detail.DockerCompose == "" {
return "", nil
}
composeMap := make(map[string]interface{}) composeMap := make(map[string]interface{})
if err := yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); err != nil { if err := yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); err != nil {
return "", err return nil, err
} }
value, ok := composeMap["services"] value, ok := composeMap["services"]
if !ok || value == nil { if !ok || value == nil {
return "", buserr.New(constant.ErrFileParse) return nil, buserr.New("ErrFileParse")
} }
servicesMap := value.(map[string]interface{}) servicesMap := value.(map[string]interface{})
if len(servicesMap) == 1 { if len(servicesMap) == 1 {
@@ -470,6 +467,34 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
delete(servicesMap, oldServiceName) delete(servicesMap, oldServiceName)
} }
} }
serviceValue := servicesMap[install.ServiceName].(map[string]interface{})
oldComposeMap := make(map[string]interface{})
if err := yaml.Unmarshal([]byte(install.DockerCompose), &oldComposeMap); err != nil {
return nil, err
}
oldValue, ok := oldComposeMap["services"]
if !ok || oldValue == nil {
return nil, buserr.New("ErrFileParse")
}
oldValueMap := oldValue.(map[string]interface{})
oldServiceValue := oldValueMap[install.ServiceName].(map[string]interface{})
if oldServiceValue["deploy"] != nil {
serviceValue["deploy"] = oldServiceValue["deploy"]
}
servicesMap[install.ServiceName] = serviceValue
composeMap["services"] = servicesMap
return composeMap, nil
}
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
if detail.DockerCompose == "" {
return "", nil
}
composeMap, err := handleUpgradeCompose(install, detail)
if err != nil {
return "", err
}
envs := make(map[string]interface{}) envs := make(map[string]interface{})
if err := json.Unmarshal([]byte(install.Env), &envs); err != nil { if err := json.Unmarshal([]byte(install.Env), &envs); err != nil {
return "", err return "", err
+49 -15
View File
@@ -413,9 +413,9 @@ func (u *BackupService) Update(req dto.BackupOperate) error {
if strings.HasSuffix(dirStr, "/") && dirStr != "/" { if strings.HasSuffix(dirStr, "/") && dirStr != "/" {
dirStr = dirStr[:strings.LastIndex(dirStr, "/")] dirStr = dirStr[:strings.LastIndex(dirStr, "/")]
} }
if err := copyDir(oldDir, dirStr); err != nil { if err := changeLocalBackup(oldDir, dirStr); err != nil {
_ = backupRepo.Update(req.ID, map[string]interface{}{"vars": oldVars}) _ = backupRepo.Update(req.ID, map[string]interface{}{"vars": oldVars})
return err return fmt.Errorf("copy dir from %s to %s failed, err: %v", oldDir, dirStr, err)
} }
global.CONF.System.Backup = dirStr global.CONF.System.Backup = dirStr
} }
@@ -518,47 +518,40 @@ func (u *BackupService) loadAccessToken(backup *model.BackupAccount) error {
} }
func (u *BackupService) loadRecordSize(records []model.BackupRecord) ([]dto.BackupFile, error) { func (u *BackupService) loadRecordSize(records []model.BackupRecord) ([]dto.BackupFile, error) {
var datas []dto.BackupFile data := make([]dto.BackupFile, len(records))
clientMap := make(map[string]loadSizeHelper) clientMap := make(map[string]loadSizeHelper)
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < len(records); i++ { for i := 0; i < len(records); i++ {
var item dto.BackupFile data[i].ID = records[i].ID
item.ID = records[i].ID data[i].Name = records[i].FileName
item.Name = records[i].FileName
itemPath := path.Join(records[i].FileDir, records[i].FileName) itemPath := path.Join(records[i].FileDir, records[i].FileName)
if _, ok := clientMap[records[i].Source]; !ok { if _, ok := clientMap[records[i].Source]; !ok {
backup, err := backupRepo.Get(commonRepo.WithByType(records[i].Source)) backup, err := backupRepo.Get(commonRepo.WithByType(records[i].Source))
if err != nil { if err != nil {
global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].Source, err) global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].Source, err)
clientMap[records[i].Source] = loadSizeHelper{} clientMap[records[i].Source] = loadSizeHelper{}
datas = append(datas, item)
continue continue
} }
client, err := u.NewClient(&backup) client, err := u.NewClient(&backup)
if err != nil { if err != nil {
global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].Source, err) global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].Source, err)
clientMap[records[i].Source] = loadSizeHelper{} clientMap[records[i].Source] = loadSizeHelper{}
datas = append(datas, item)
continue continue
} }
item.Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath)) data[i].Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath))
datas = append(datas, item)
clientMap[records[i].Source] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true} clientMap[records[i].Source] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true}
continue continue
} }
if clientMap[records[i].Source].isOk { if clientMap[records[i].Source].isOk {
wg.Add(1) wg.Add(1)
go func(index int) { go func(index int) {
item.Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath)) data[i].Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath))
datas = append(datas, item)
wg.Done() wg.Done()
}(i) }(i)
} else {
datas = append(datas, item)
} }
} }
wg.Wait() wg.Wait()
return datas, nil return data, nil
} }
func loadLocalDir() (string, error) { func loadLocalDir() (string, error) {
@@ -684,3 +677,44 @@ func (u *BackupService) Run() {
}).Error }).Error
global.LOG.Info("Successfully refreshed OneDrive token.") global.LOG.Info("Successfully refreshed OneDrive token.")
} }
func changeLocalBackup(oldPath, newPath string) error {
fileOp := fileUtils.NewFileOp()
if fileOp.Stat(path.Join(oldPath, "app")) {
if err := fileOp.CopyDir(path.Join(oldPath, "app"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "database")) {
if err := fileOp.CopyDir(path.Join(oldPath, "database"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "directory")) {
if err := fileOp.CopyDir(path.Join(oldPath, "directory"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "system_snapshot")) {
if err := fileOp.CopyDir(path.Join(oldPath, "system_snapshot"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "website")) {
if err := fileOp.CopyDir(path.Join(oldPath, "website"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "log")) {
if err := fileOp.CopyDir(path.Join(oldPath, "log"), newPath); err != nil {
return err
}
}
_ = fileOp.RmRf(path.Join(oldPath, "app"))
_ = fileOp.RmRf(path.Join(oldPath, "database"))
_ = fileOp.RmRf(path.Join(oldPath, "directory"))
_ = fileOp.RmRf(path.Join(oldPath, "system_snapshot"))
_ = fileOp.RmRf(path.Join(oldPath, "website"))
_ = fileOp.RmRf(path.Join(oldPath, "log"))
return nil
}
+103 -129
View File
@@ -1,7 +1,6 @@
package service package service
import ( import (
"bufio"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -28,14 +27,14 @@ import (
) )
const ( const (
clamServiceNameCentOs = "clamd@scan.service" clamServiceKey = "clam"
clamServiceNameUbuntu = "clamav-daemon.service" freshClamServiceKey = "freshclam"
freshClamService = "clamav-freshclam.service" resultDir = "clamav"
resultDir = "clamav"
) )
type ClamService struct { type ClamService struct {
serviceName string serviceName string
freshClamService string
} }
type IClamService interface { type IClamService interface {
@@ -63,23 +62,32 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
var baseInfo dto.ClamBaseInfo var baseInfo dto.ClamBaseInfo
baseInfo.Version = "-" baseInfo.Version = "-"
baseInfo.FreshVersion = "-" baseInfo.FreshVersion = "-"
exist1, _ := systemctl.IsExist(clamServiceNameCentOs) clamSvc, err := systemctl.GetServiceName(clamServiceKey)
if exist1 { if err != nil {
c.serviceName = clamServiceNameCentOs baseInfo.IsExist = false
baseInfo.IsExist = true return baseInfo, nil
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs)
} }
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu) c.serviceName = clamSvc
if exist2 { isExist, err := systemctl.IsExist(c.serviceName)
c.serviceName = clamServiceNameUbuntu if err != nil {
baseInfo.IsExist = true baseInfo.IsExist = false
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
} }
freshExist, _ := systemctl.IsExist(freshClamService) baseInfo.IsExist = isExist
if freshExist { baseInfo.IsActive, _ = systemctl.IsActive(clamSvc)
baseInfo.FreshIsExist = true
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService) freshSvc, err := systemctl.GetServiceName(freshClamServiceKey)
if err != nil {
baseInfo.FreshIsExist = false
return baseInfo, nil
} }
c.freshClamService = freshSvc
freshisExist, err := systemctl.IsExist(c.freshClamService)
if err != nil {
baseInfo.FreshIsExist = false
}
baseInfo.FreshIsExist = freshisExist
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshSvc)
if !cmd.Which("clamdscan") { if !cmd.Which("clamdscan") {
baseInfo.IsActive = false baseInfo.IsActive = false
} }
@@ -110,22 +118,27 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
} }
func (c *ClamService) Operate(operate string) error { func (c *ClamService) Operate(operate string) error {
var err error
switch operate { switch operate {
case "start", "restart", "stop": case "start":
stdout, err := cmd.Execf("systemctl %s %s", operate, c.serviceName) err = systemctl.Start(c.serviceName)
if err != nil { case "stop":
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout) err = systemctl.Stop(c.serviceName)
} case "restart":
return nil err = systemctl.Restart(c.serviceName)
case "fresh-start", "fresh-restart", "fresh-stop": case "fresh-start":
stdout, err := cmd.Execf("systemctl %s %s", strings.TrimPrefix(operate, "fresh-"), freshClamService) err = systemctl.Start(c.freshClamService)
if err != nil { case "fresh-stop":
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout) err = systemctl.Stop(c.freshClamService)
} case "fresh-restart":
return nil err = systemctl.Restart(c.freshClamService)
default: default:
return fmt.Errorf("not support such operation: %v", operate) return fmt.Errorf("unsupported operation: %s", operate)
} }
if err != nil {
return fmt.Errorf("%s %s failed: %v", operate, c.serviceName, err)
}
return nil
} }
func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) { func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) {
@@ -432,102 +445,80 @@ func (c *ClamService) LoadFile(req dto.ClamFileReq) (string, error) {
filePath := "" filePath := ""
switch req.Name { switch req.Name {
case "clamd": case "clamd":
if c.serviceName == clamServiceNameUbuntu { filePath = c.getConfigPath("clamd")
filePath = "/etc/clamav/clamd.conf"
} else {
filePath = "/etc/clamd.d/scan.conf"
}
case "clamd-log": case "clamd-log":
filePath = c.loadLogPath("clamd-log") filePath = c.loadLogPath("clamd-log")
if len(filePath) != 0 {
break
}
if c.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/clamav.log"
} else {
filePath = "/var/log/clamd.scan"
}
case "freshclam": case "freshclam":
if c.serviceName == clamServiceNameUbuntu { filePath = c.getConfigPath("freshclam")
filePath = "/etc/clamav/freshclam.conf"
} else {
filePath = "/etc/freshclam.conf"
}
case "freshclam-log": case "freshclam-log":
filePath = c.loadLogPath("freshclam-log") filePath = c.loadLogPath("freshclam-log")
if len(filePath) != 0 {
break
}
if c.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/freshclam.log"
} else {
filePath = "/var/log/freshclam.log"
}
default: default:
return "", fmt.Errorf("not support such type") return "", fmt.Errorf("unsupported file type")
} }
if _, err := os.Stat(filePath); err != nil {
content, err := systemctl.ViewConfig(filePath, systemctl.ConfigOption{TailLines: req.Tail})
if err != nil {
return "", buserr.New("ErrHttpReqNotFound") return "", buserr.New("ErrHttpReqNotFound")
} }
var tail string return content, nil
if req.Tail != "0" {
tail = req.Tail
} else {
tail = "+1"
}
cmd := exec.Command("tail", "-n", tail, filePath)
stdout, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err)
}
return string(stdout), nil
} }
func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error { func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
filePath := "" var (
service := "" filePath string
service string
)
switch req.Name { switch req.Name {
case "clamd": case "clamd":
if c.serviceName == clamServiceNameUbuntu { filePath = c.getConfigPath("clamd")
service = clamServiceNameUbuntu service = c.serviceName
filePath = "/etc/clamav/clamd.conf"
} else {
service = clamServiceNameCentOs
filePath = "/etc/clamd.d/scan.conf"
}
case "freshclam": case "freshclam":
if c.serviceName == clamServiceNameUbuntu { filePath = c.getConfigPath("freshclam")
filePath = "/etc/clamav/freshclam.conf" service = c.freshClamService
} else {
filePath = "/etc/freshclam.conf"
}
service = "clamav-freshclam.service"
default: default:
return fmt.Errorf("not support such type") return fmt.Errorf("unsupported file type")
} }
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640) file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(req.File)
write.Flush()
_ = systemctl.Restart(service) if _, err := file.WriteString(req.File); err != nil {
return err
}
if err := systemctl.Restart(service); err != nil {
return fmt.Errorf("restart %s failed: %v", service, err)
}
return nil return nil
} }
func (c *ClamService) getConfigPath(confType string) string {
switch confType {
case "clamd":
if _, err := os.Stat("/etc/clamav/clamd.conf"); err == nil {
return "/etc/clamav/clamd.conf"
}
return "/etc/clamd.d/scan.conf"
case "freshclam":
if _, err := os.Stat("/etc/clamav/freshclam.conf"); err == nil {
return "/etc/clamav/freshclam.conf"
}
return "/etc/freshclam.conf"
default:
return ""
}
}
func StopAllCronJob(withCheck bool) bool { func StopAllCronJob(withCheck bool) bool {
if withCheck { if withCheck {
isActive := false isActive := false
exist1, _ := systemctl.IsExist(clamServiceNameCentOs) isexist, _ := systemctl.IsExist(clamServiceKey)
if exist1 { if isexist {
isActive, _ = systemctl.IsActive(clamServiceNameCentOs) isActive, _ = systemctl.IsActive(clamServiceKey)
}
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
if exist2 {
isActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
} }
if isActive { if isActive {
return false return false
@@ -590,42 +581,25 @@ func loadResultFromLog(pathItem string) dto.ClamLog {
return data return data
} }
func (c *ClamService) loadLogPath(name string) string { func (c *ClamService) loadLogPath(name string) string {
confPath := "" configKey := "clamd"
if name == "clamd-log" { searchPrefix := "LogFile "
if c.serviceName == clamServiceNameUbuntu { if name != "clamd-log" {
confPath = "/etc/clamav/clamd.conf" configKey = "freshclam"
} else { searchPrefix = "UpdateLogFile "
confPath = "/etc/clamd.d/scan.conf"
}
} else {
if c.serviceName == clamServiceNameUbuntu {
confPath = "/etc/clamav/freshclam.conf"
} else {
confPath = "/etc/freshclam.conf"
}
}
if _, err := os.Stat(confPath); err != nil {
return ""
} }
confPath := c.getConfigPath(configKey)
content, err := os.ReadFile(confPath) content, err := os.ReadFile(confPath)
if err != nil { if err != nil {
global.LOG.Debugf("Failed to read %s config: %v", configKey, err)
return "" return ""
} }
lines := strings.Split(string(content), "\n")
if name == "clamd-log" { for _, line := range strings.Split(string(content), "\n") {
for _, line := range lines { if strings.HasPrefix(line, searchPrefix) {
if strings.HasPrefix(line, "LogFile ") { return strings.TrimSpace(strings.TrimPrefix(line, searchPrefix))
return strings.Trim(strings.ReplaceAll(line, "LogFile ", ""), " ")
}
}
} else {
for _, line := range lines {
if strings.HasPrefix(line, "UpdateLogFile ") {
return strings.Trim(strings.ReplaceAll(line, "UpdateLogFile ", ""), " ")
}
} }
} }
return "" return ""
} }
+9 -13
View File
@@ -450,12 +450,14 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
networkSettings := oldContainer.NetworkSettings networkSettings := oldContainer.NetworkSettings
bridgeNetworkSettings := networkSettings.Networks[data.Network] bridgeNetworkSettings := networkSettings.Networks[data.Network]
if bridgeNetworkSettings.IPAMConfig != nil { if bridgeNetworkSettings.IPAMConfig != nil {
ipv4Address := bridgeNetworkSettings.IPAMConfig.IPv4Address if data.Network != "bridge" {
data.Ipv4 = ipv4Address data.Ipv4 = bridgeNetworkSettings.IPAMConfig.IPv4Address
ipv6Address := bridgeNetworkSettings.IPAMConfig.IPv6Address data.Ipv6 = bridgeNetworkSettings.IPAMConfig.IPv6Address
data.Ipv6 = ipv6Address }
} else { } else {
data.Ipv4 = bridgeNetworkSettings.IPAddress if data.Network != "bridge" {
data.Ipv4 = bridgeNetworkSettings.IPAddress
}
} }
data.Cmd = oldContainer.Config.Cmd data.Cmd = oldContainer.Config.Cmd
@@ -556,10 +558,7 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
hostConf := oldContainer.HostConfig hostConf := oldContainer.HostConfig
var networkConf network.NetworkingConfig var networkConf network.NetworkingConfig
if oldContainer.NetworkSettings != nil { if oldContainer.NetworkSettings != nil {
for networkKey := range oldContainer.NetworkSettings.Networks { networkConf.EndpointsConfig = oldContainer.NetworkSettings.Networks
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
}
} }
if err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}); err != nil { if err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}); err != nil {
return err return err
@@ -1213,10 +1212,7 @@ func reCreateAfterUpdate(name string, client *client.Client, config *container.C
var oldNetworkConf network.NetworkingConfig var oldNetworkConf network.NetworkingConfig
if networkConf != nil { if networkConf != nil {
for networkKey := range networkConf.Networks { oldNetworkConf.EndpointsConfig = networkConf.Networks
oldNetworkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
}
} }
oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name) oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name)
+11 -2
View File
@@ -114,8 +114,17 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
if err != nil { if err != nil {
return err return err
} }
if !req.CleanRemoteData {
for key := range accountMap {
if key != constant.Local {
delete(accountMap, key)
}
}
}
cronjob.RetainCopies = 0 cronjob.RetainCopies = 0
u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{}) if len(accountMap) != 0 {
u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{})
}
} else { } else {
u.removeExpiredLog(cronjob) u.removeExpiredLog(cronjob)
} }
@@ -249,7 +258,7 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
global.Cron.Remove(cron.EntryID(idItem)) global.Cron.Remove(cron.EntryID(idItem))
} }
global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs) global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs)
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, IsDelete: true}); err != nil { if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, CleanRemoteData: req.CleanRemoteData, IsDelete: true}); err != nil {
return err return err
} }
if err := cronjobRepo.Delete(commonRepo.WithByID(id)); err != nil { if err := cronjobRepo.Delete(commonRepo.WithByID(id)); err != nil {
+8 -1
View File
@@ -322,7 +322,8 @@ func (u *CronjobService) uploadCronjobBackFile(cronjob model.Cronjob, accountMap
if len(account) != 0 { if len(account) != 0 {
global.LOG.Debugf("start upload file to %s, dir: %s", account, path.Join(accountMap[account].backupPath, cloudSrc)) global.LOG.Debugf("start upload file to %s, dir: %s", account, path.Join(accountMap[account].backupPath, cloudSrc))
if _, err := accountMap[account].client.Upload(file, path.Join(accountMap[account].backupPath, cloudSrc)); err != nil { if _, err := accountMap[account].client.Upload(file, path.Join(accountMap[account].backupPath, cloudSrc)); err != nil {
return "", err global.LOG.Errorf("upload file to %s failed, err: %v", account, err)
continue
} }
global.LOG.Debugf("upload successful!") global.LOG.Debugf("upload successful!")
} }
@@ -349,12 +350,18 @@ func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap m
if cronjob.Type == "snapshot" { if cronjob.Type == "snapshot" {
for _, account := range accounts { for _, account := range accounts {
if len(account) != 0 { if len(account) != 0 {
if _, ok := accountMap[account]; !ok {
continue
}
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, "system_snapshot", records[i].FileName)) _, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, "system_snapshot", records[i].FileName))
} }
} }
_ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(records[i].FileName, ".tar.gz"))) _ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(records[i].FileName, ".tar.gz")))
} else { } else {
for _, account := range accounts { for _, account := range accounts {
if _, ok := accountMap[account]; !ok {
continue
}
if len(account) != 0 { if len(account) != 0 {
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, records[i].FileDir, records[i].FileName)) _, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, records[i].FileDir, records[i].FileName))
} }
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/ntp" "github.com/1Panel-dev/1Panel/backend/utils/ntp"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/shirou/gopsutil/v3/mem" "github.com/shirou/gopsutil/v3/mem"
) )
@@ -121,7 +122,7 @@ func (u *DeviceService) Update(key, value string) error {
return err return err
} }
go func() { go func() {
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system for new time zone failed, err: %v", err) global.LOG.Errorf("restart system for new time zone failed, err: %v", err)
} }
+2 -2
View File
@@ -11,12 +11,12 @@ import (
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/docker" "github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
fileUtils "github.com/1Panel-dev/1Panel/backend/utils/files" fileUtils "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/google/uuid" "github.com/google/uuid"
@@ -285,7 +285,7 @@ func (u *DeviceService) Clean(req []dto.Clean) {
if restart { if restart {
go func() { go func() {
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system port failed, err: %v", err) global.LOG.Errorf("restart system port failed, err: %v", err)
} }
+30 -26
View File
@@ -365,17 +365,18 @@ func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
func (u *DockerService) OperateDocker(req dto.DockerOperation) error { func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
service := "docker" service := "docker"
sudo := cmd.SudoHandleCmd() h, err := systemctl.DefaultHandler(service)
dockerCmd, err := getDockerRestartCommand()
if err != nil { if err != nil {
return err return err
} }
if req.Operation == "stop" { if req.Operation == "stop" {
isSocketActive, _ := systemctl.IsActive("docker.socket") socketHandle, err := systemctl.DefaultHandler("docker.socket")
if isSocketActive { if err == nil {
std, err := cmd.Execf("%s systemctl stop docker.socket", sudo) status, err := socketHandle.IsActive()
if err != nil { if err == nil && status.IsActive {
global.LOG.Errorf("handle systemctl stop docker.socket failed, err: %v", std) if std, err := socketHandle.ExecuteAction("stop"); err != nil {
global.LOG.Errorf("handle stop docker.socket failed, err: %v", std)
}
} }
} }
} }
@@ -386,13 +387,20 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
} }
} }
stdout, err := cmd.Execf("%s %s %s", dockerCmd, req.Operation, service) if isDockerSnapInstalled() {
command := fmt.Sprintf("snap %s docker", req.Operation)
stdout, err := cmd.Exec(command)
if err != nil {
return fmt.Errorf("failed to restart docker: %v", stdout)
}
return nil
}
result, err := h.ExecuteAction(req.Operation)
if err != nil { if err != nil {
return errors.New(stdout) return errors.New(result.Output)
} }
return nil return nil
} }
func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize string) { func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize string) {
if opts, ok := daemonMap["log-opts"]; ok { if opts, ok := daemonMap["log-opts"]; ok {
if len(logMaxFile) != 0 || len(logMaxSize) != 0 { if len(logMaxFile) != 0 || len(logMaxSize) != 0 {
@@ -451,31 +459,27 @@ func validateDockerConfig() error {
return nil return nil
} }
if err != nil || (stdout != "" && strings.TrimSpace(stdout) != "configuration OK") { if err != nil || (stdout != "" && strings.TrimSpace(stdout) != "configuration OK") {
return fmt.Errorf("Docker configuration validation failed, err: %v", stdout) return fmt.Errorf("docker configuration validation failed, err: %v", stdout)
} }
return nil return nil
} }
func getDockerRestartCommand() (string, error) { func isDockerSnapInstalled() bool {
stdout, err := cmd.Exec("which docker") stdout, err := cmd.Exec("which docker")
if err != nil { if err != nil {
return "", fmt.Errorf("failed to find docker: %v", err) return false
} }
dockerPath := stdout stdout = strings.TrimSpace(stdout)
if strings.Contains(dockerPath, "snap") { return strings.Contains(stdout, "snap")
return "snap", nil
}
return "systemctl", nil
} }
func restartDocker() error { func restartDocker() error {
restartCmd, err := getDockerRestartCommand() if isDockerSnapInstalled() {
if err != nil { stdout, err := cmd.Exec("snap restart docker")
return err if err != nil {
return fmt.Errorf("failed to restart docker: %v", stdout)
}
return nil
} }
stdout, err := cmd.Execf("%s restart docker", restartCmd) return systemctl.Restart("docker")
if err != nil {
return fmt.Errorf("failed to restart Docker: %s", stdout)
}
return nil
} }
+2
View File
@@ -46,4 +46,6 @@ var (
phpExtensionsRepo = repo.NewIPHPExtensionsRepo() phpExtensionsRepo = repo.NewIPHPExtensionsRepo()
favoriteRepo = repo.NewIFavoriteRepo() favoriteRepo = repo.NewIFavoriteRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
) )
+10 -1
View File
@@ -366,10 +366,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
return buserr.New(constant.ErrMovePathFailed) return buserr.New(constant.ErrMovePathFailed)
} }
} }
var errs []error
if m.Type == "cut" { if m.Type == "cut" {
if len(m.CoverPaths) > 0 {
for _, src := range m.CoverPaths {
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
errs = append(errs, err)
global.LOG.Errorf("cut copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
}
}
}
return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover) return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover)
} }
var errs []error
if m.Type == "copy" { if m.Type == "copy" {
for _, src := range m.OldPaths { for _, src := range m.OldPaths {
if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil { if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil {
@@ -518,6 +526,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
Name: info.Name(), Name: info.Name(),
Path: filePath, Path: filePath,
ModTime: info.ModTime(), ModTime: info.ModTime(),
IsDir: info.IsDir(),
}) })
} }
} }
+119 -89
View File
@@ -3,6 +3,12 @@ package service
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os/exec"
"os/user"
"path"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/dto/request" "github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response" "github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
@@ -14,11 +20,6 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/systemctl" "github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"os/exec"
"os/user"
"path"
"strconv"
"strings"
) )
type HostToolService struct{} type HostToolService struct{}
@@ -39,93 +40,108 @@ func NewIHostToolService() IHostToolService {
} }
func (h *HostToolService) GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) { func (h *HostToolService) GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) {
res := &response.HostToolRes{} res := &response.HostToolRes{Type: req.Type}
res.Type = req.Type
switch req.Type { switch req.Type {
case constant.Supervisord: case constant.Supervisord:
supervisorConfig := &response.Supervisor{} return h.getSupervisorStatus(res)
if !cmd.Which(constant.Supervisord) {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
}
supervisorConfig.IsExist = true
serviceExist, _ := systemctl.IsExist(constant.Supervisord)
if !serviceExist {
serviceExist, _ = systemctl.IsExist(constant.Supervisor)
if !serviceExist {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
} else {
supervisorConfig.ServiceName = constant.Supervisor
}
} else {
supervisorConfig.ServiceName = constant.Supervisord
}
serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName))
if serviceNameSet.ID != 0 || serviceNameSet.Value != "" {
supervisorConfig.ServiceName = serviceNameSet.Value
}
versionRes, _ := cmd.Exec("supervisord -v")
supervisorConfig.Version = strings.TrimSuffix(versionRes, "\n")
_, ctlRrr := exec.LookPath("supervisorctl")
supervisorConfig.CtlExist = ctlRrr == nil
active, _ := systemctl.IsActive(supervisorConfig.ServiceName)
if active {
supervisorConfig.Status = "running"
} else {
supervisorConfig.Status = "stopped"
}
pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath))
if pathSet.ID != 0 || pathSet.Value != "" {
supervisorConfig.ConfigPath = pathSet.Value
res.Config = supervisorConfig
return res, nil
} else {
supervisorConfig.Init = true
}
servicePath := "/usr/lib/systemd/system/supervisor.service"
fileOp := files.NewFileOp()
if !fileOp.Stat(servicePath) {
servicePath = "/usr/lib/systemd/system/supervisord.service"
}
if fileOp.Stat(servicePath) {
startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart")
if startCmd != "" {
args := strings.Fields(startCmd)
cIndex := -1
for i, arg := range args {
if arg == "-c" {
cIndex = i
break
}
}
if cIndex != -1 && cIndex+1 < len(args) {
supervisorConfig.ConfigPath = args[cIndex+1]
}
}
}
if supervisorConfig.ConfigPath == "" {
configPath := "/etc/supervisord.conf"
if !fileOp.Stat(configPath) {
configPath = "/etc/supervisor/supervisord.conf"
if fileOp.Stat(configPath) {
supervisorConfig.ConfigPath = configPath
}
}
}
res.Config = supervisorConfig
} }
return res, nil return res, nil
} }
func (h *HostToolService) getSupervisorStatus(res *response.HostToolRes) (*response.HostToolRes, error) {
supervisorConfig := &response.Supervisor{}
if !cmd.Which(constant.Supervisord) {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
}
supervisorConfig.IsExist = true
serviceName, err := h.determineServiceName()
if err != nil || serviceName == "" {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
}
supervisorConfig.ServiceName = serviceName
if nameSetting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)); nameSetting.Value != "" {
supervisorConfig.ServiceName = nameSetting.Value
}
if version, err := cmd.Exec("supervisord -v"); err == nil {
supervisorConfig.Version = strings.TrimSpace(version)
}
_, errCtl := exec.LookPath("supervisorctl")
supervisorConfig.CtlExist = errCtl == nil
if active, err := systemctl.IsActive(supervisorConfig.ServiceName); err == nil && active {
supervisorConfig.Status = "running"
} else {
supervisorConfig.Status = "stopped"
}
h.resolveConfigPath(supervisorConfig)
res.Config = supervisorConfig
return res, nil
}
func (h *HostToolService) determineServiceName() (string, error) {
if setting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)); setting.Value != "" {
serviceName, err := systemctl.GetServiceName(setting.Value)
if err != nil {
return "", fmt.Errorf("get service name error: %v", err)
}
return serviceName, nil
}
if serviceName, err := systemctl.GetServiceName(constant.Supervisord); err == nil {
return serviceName, nil
}
return systemctl.GetServiceName(constant.Supervisor)
}
func (h *HostToolService) resolveConfigPath(config *response.Supervisor) {
if pathSetting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)); pathSetting.Value != "" {
config.ConfigPath = pathSetting.Value
return
}
config.Init = true
if servicePath, err := systemctl.GetServicePath(config.ServiceName); err == nil {
if startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart"); startCmd != "" {
if path := parseConfigPathFromCommand(startCmd); path != "" {
config.ConfigPath = path
return
}
}
}
defaultPaths := []string{
"/etc/supervisord.conf",
"/etc/supervisor/supervisord.conf",
"/usr/local/etc/supervisord.conf",
}
for _, path := range defaultPaths {
if systemctl.FileExist(path) {
config.ConfigPath = path
return
}
}
}
func parseConfigPathFromCommand(cmd string) string {
parts := strings.Fields(cmd)
for i, part := range parts {
if part == "-c" && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}
func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error { func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error {
switch req.Type { switch req.Type {
case constant.Supervisord: case constant.Supervisord:
@@ -200,14 +216,28 @@ func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error {
} }
func (h *HostToolService) OperateTool(req request.HostToolReq) error { func (h *HostToolService) OperateTool(req request.HostToolReq) error {
serviceName := req.Type var serviceName string
if req.Type == constant.Supervisord { var err error
switch req.Type {
case constant.Supervisord:
serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName))
if serviceNameSet.ID != 0 || serviceNameSet.Value != "" { if serviceNameSet.ID != 0 || serviceNameSet.Value != "" {
serviceName = serviceNameSet.Value serviceName = serviceNameSet.Value
} else {
serviceName = req.Type
} }
default:
serviceName = req.Type
} }
return systemctl.Operate(req.Operate, serviceName) serviceName, err = systemctl.GetServiceName(serviceName)
if err != nil {
if errors.Is(err, systemctl.ErrServiceNotFound) {
return fmt.Errorf("%s service unavailable. Please ensure supervisor server is installed and configured", serviceName)
}
return err
}
_, err = systemctl.CustomAction(req.Operate, serviceName)
return err
} }
func (h *HostToolService) OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) { func (h *HostToolService) OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) {
+73 -47
View File
@@ -26,7 +26,7 @@ type IImageRepoService interface {
Login(req dto.OperateByID) error Login(req dto.OperateByID) error
Create(req dto.ImageRepoCreate) error Create(req dto.ImageRepoCreate) error
Update(req dto.ImageRepoUpdate) error Update(req dto.ImageRepoUpdate) error
BatchDelete(req dto.ImageRepoDelete) error Delete(req dto.OperateByID) error
} }
func NewIImageRepoService() IImageRepoService { func NewIImageRepoService() IImageRepoService {
@@ -89,31 +89,7 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil { if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err) return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
} }
if err := validateDockerConfig(); err != nil { if err := stopBeforeUpdateRepo(); err != nil {
return err
}
if err := restartDocker(); err != nil {
return err
}
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
if err := func() error {
for range ticker.C {
select {
case <-ctx.Done():
cancel()
return errors.New("the docker service cannot be restarted")
default:
stdout, err := cmd.Exec("systemctl is-active docker")
if string(stdout) == "active\n" && err == nil {
global.LOG.Info("docker restart with new conf successful!")
return nil
}
}
}
return nil
}(); err != nil {
return err return err
} }
} }
@@ -131,15 +107,32 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
return imageRepoRepo.Create(&imageRepo) return imageRepoRepo.Create(&imageRepo)
} }
func (u *ImageRepoService) BatchDelete(req dto.ImageRepoDelete) error { func (u *ImageRepoService) Delete(req dto.OperateByID) error {
for _, id := range req.Ids { if req.ID == 1 {
if id == 1 { return errors.New("The default value cannot be edit !")
return errors.New("The default value cannot be edit !")
}
} }
if err := imageRepoRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil { itemRepo, _ := imageRepoRepo.Get(commonRepo.WithByID(req.ID))
if itemRepo.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if itemRepo.Auth {
_, _ = cmd.Execf("docker logout -i %s", itemRepo.DownloadUrl)
}
if itemRepo.Protocol == "https" {
return imageRepoRepo.Delete(commonRepo.WithByID(req.ID))
}
if err := u.handleRegistries("", itemRepo.DownloadUrl, "delete"); err != nil {
return fmt.Errorf("delete registry %s failed, err: %v", itemRepo.DownloadUrl, err)
}
if err := validateDockerConfig(); err != nil {
return err return err
} }
if err := imageRepoRepo.Delete(commonRepo.WithByID(req.ID)); err != nil {
return err
}
go func() {
_ = restartDocker()
}()
return nil return nil
} }
@@ -154,37 +147,39 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
if err != nil { if err != nil {
return err return err
} }
needRestart := false
if repo.Protocol == "http" && req.Protocol == "https" { if repo.Protocol == "http" && req.Protocol == "https" {
if err := u.handleRegistries("", repo.DownloadUrl, "delete"); err != nil { if err := u.handleRegistries("", repo.DownloadUrl, "delete"); err != nil {
return fmt.Errorf("delete registry %s failed, err: %v", repo.DownloadUrl, err) return fmt.Errorf("delete registry %s failed, err: %v", repo.DownloadUrl, err)
} }
needRestart = true
} }
if repo.Protocol == "http" && req.Protocol == "http" { if repo.Protocol == "http" && req.Protocol == "http" {
if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil { if repo.DownloadUrl != req.DownloadUrl {
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err) if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil {
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err)
}
needRestart = true
} }
} }
if repo.Protocol == "https" && req.Protocol == "http" { if repo.Protocol == "https" && req.Protocol == "http" {
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil { if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err) return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
} }
needRestart = true
} }
if repo.Auth != req.Auth || repo.DownloadUrl != req.DownloadUrl { if needRestart {
if repo.Auth { if err := stopBeforeUpdateRepo(); err != nil {
_, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl) return err
}
if req.Auth {
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
return err
}
} }
} }
if repo.Auth {
if err := validateDockerConfig(); err != nil { _, _ = cmd.Execf("docker logout -i %s", repo.DownloadUrl)
return err
} }
if err := restartDocker(); err != nil { if req.Auth {
return err if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
return err
}
} }
upMap := make(map[string]interface{}) upMap := make(map[string]interface{})
@@ -256,3 +251,34 @@ func (u *ImageRepoService) handleRegistries(newHost, delHost, handle string) err
} }
return nil return nil
} }
func stopBeforeUpdateRepo() error {
if err := validateDockerConfig(); err != nil {
return err
}
if err := restartDocker(); err != nil {
return err
}
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
if err := func() error {
for range ticker.C {
select {
case <-ctx.Done():
cancel()
return errors.New("the docker service cannot be restarted")
default:
stdout, err := cmd.Exec("systemctl is-active docker")
if string(stdout) == "active\n" && err == nil {
global.LOG.Info("docker restart with new conf successful!")
return nil
}
}
}
return nil
}(); err != nil {
return err
}
return nil
}
+643
View File
@@ -0,0 +1,643 @@
package service
import (
"context"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/nginx"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/components"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/parser"
"github.com/1Panel-dev/1Panel/cmd/server/mcp"
"github.com/1Panel-dev/1Panel/cmd/server/nginx_conf"
"github.com/subosito/gotenv"
"gopkg.in/yaml.v3"
"path"
"strconv"
"strings"
)
type McpServerService struct{}
type IMcpServerService interface {
Page(req request.McpServerSearch) response.McpServersRes
Create(create request.McpServerCreate) error
Update(req request.McpServerUpdate) error
Delete(id uint) error
Operate(req request.McpServerOperate) error
GetBindDomain() (response.McpBindDomainRes, error)
BindDomain(req request.McpBindDomain) error
UpdateBindDomain(req request.McpBindDomainUpdate) error
}
func NewIMcpServerService() IMcpServerService {
return &McpServerService{}
}
func (m McpServerService) Page(req request.McpServerSearch) response.McpServersRes {
var (
res response.McpServersRes
items []response.McpServerDTO
)
total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
for _, item := range data {
_ = syncMcpServerContainerStatus(&item)
serverDTO := response.McpServerDTO{
McpServer: item,
Environments: make([]request.Environment, 0),
Volumes: make([]request.Volume, 0),
}
project, _ := docker.GetComposeProject(item.Name, path.Join(constant.McpDir, item.Name), []byte(item.DockerCompose), []byte(item.Env), true)
for _, service := range project.Services {
if service.Environment != nil {
for key, value := range service.Environment {
serverDTO.Environments = append(serverDTO.Environments, request.Environment{
Key: key,
Value: *value,
})
}
}
if service.Volumes != nil {
for _, volume := range service.Volumes {
serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
Source: volume.Source,
Target: volume.Target,
})
}
}
}
items = append(items, serverDTO)
}
res.Total = total
res.Items = items
return res
}
func (m McpServerService) Update(req request.McpServerUpdate) error {
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
if mcpServer.Port != req.Port {
if err := checkPortExist(req.Port); err != nil {
return err
}
}
if mcpServer.ContainerName != req.ContainerName {
if err := checkContainerName(req.ContainerName); err != nil {
return err
}
}
mcpServer.Name = req.Name
mcpServer.ContainerName = req.ContainerName
mcpServer.Port = req.Port
mcpServer.Command = req.Command
mcpServer.BaseURL = req.BaseURL
mcpServer.SsePath = req.SsePath
mcpServer.HostIP = req.HostIP
if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil {
return err
}
env := handleEnv(mcpServer)
mcpDir := path.Join(constant.McpDir, mcpServer.Name)
envPath := path.Join(mcpDir, ".env")
if err := gotenv.Write(env, envPath); err != nil {
return err
}
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
if err := files.NewFileOp().SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
return err
}
mcpServer.Status = constant.RuntimeStarting
if err := mcpServerRepo.Save(mcpServer); err != nil {
return err
}
go startMcp(mcpServer)
return nil
}
func (m McpServerService) Create(create request.McpServerCreate) error {
servers, _ := mcpServerRepo.List()
for _, server := range servers {
if server.Port == create.Port {
return buserr.New("ErrPortInUsed")
}
if server.ContainerName == create.ContainerName {
return buserr.New("ErrContainerName")
}
if server.Name == create.Name {
return buserr.New(constant.ErrNameIsExist)
}
if server.SsePath == create.SsePath {
return buserr.New("ErrSsePath")
}
}
if err := checkPortExist(create.Port); err != nil {
return err
}
if err := checkContainerName(create.ContainerName); err != nil {
return err
}
mcpDir := path.Join(constant.McpDir, create.Name)
mcpServer := &model.McpServer{
Name: create.Name,
ContainerName: create.ContainerName,
Port: create.Port,
Command: create.Command,
Status: constant.RuntimeStarting,
BaseURL: create.BaseURL,
SsePath: create.SsePath,
Dir: mcpDir,
HostIP: create.HostIP,
}
if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil {
return err
}
env := handleEnv(mcpServer)
filesOP := files.NewFileOp()
if !filesOP.Stat(mcpDir) {
_ = filesOP.CreateDir(mcpDir, 0644)
}
envPath := path.Join(mcpDir, ".env")
if err := gotenv.Write(env, envPath); err != nil {
return err
}
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
if err := filesOP.SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
return err
}
if err := mcpServerRepo.Create(mcpServer); err != nil {
return err
}
addProxy(mcpServer)
go startMcp(mcpServer)
return nil
}
func (m McpServerService) Delete(id uint) error {
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return err
}
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
_ = files.NewFileOp().DeleteDir(path.Join(constant.McpDir, mcpServer.Name))
websiteID := GetWebsiteID()
if websiteID > 0 {
websiteService := NewIWebsiteService()
delProxyReq := request.WebsiteProxyDel{
ID: websiteID,
Name: mcpServer.Name,
}
_ = websiteService.DeleteProxy(delProxyReq)
}
return mcpServerRepo.DeleteBy(commonRepo.WithByID(id))
}
func (m McpServerService) Operate(req request.McpServerOperate) error {
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
var out string
switch req.Operate {
case "start":
out, err = compose.Up(composePath)
mcpServer.Status = constant.RuntimeRunning
case "stop":
out, err = compose.Down(composePath)
mcpServer.Status = constant.RuntimeStopped
case "restart":
out, err = compose.Restart(composePath)
mcpServer.Status = constant.RuntimeRunning
}
if err != nil {
mcpServer.Status = constant.RuntimeError
mcpServer.Message = out
}
return mcpServerRepo.Save(mcpServer)
}
func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) {
var res response.McpBindDomainRes
websiteID := GetWebsiteID()
if websiteID == 0 {
return res, nil
}
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
if err != nil {
return res, nil
}
res.WebsiteID = website.ID
res.Domain = website.PrimaryDomain
if website.WebsiteSSLID > 0 {
res.SSLID = website.WebsiteSSLID
ssl, _ := websiteSSLRepo.GetFirst(commonRepo.WithByID(website.WebsiteSSLID))
res.AcmeAccountID = ssl.AcmeAccountID
}
res.ConnUrl = fmt.Sprintf("%s://%s", strings.ToLower(website.Protocol), website.PrimaryDomain)
res.AllowIPs = GetAllowIps(website)
return res, nil
}
func (m McpServerService) BindDomain(req request.McpBindDomain) error {
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
if nginxInstall.ID == 0 {
return buserr.New("ErrOpenrestyInstall")
}
var (
ipList []string
err error
)
if len(req.IPList) > 0 {
ipList, err = common.HandleIPList(req.IPList)
if err != nil {
return err
}
}
if req.SSLID > 0 {
ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
group, _ := groupRepo.Get(groupRepo.WithByWebsiteDefault())
createWebsiteReq := request.WebsiteCreate{
PrimaryDomain: req.Domain,
Alias: strings.ToLower(req.Domain),
Type: constant.Static,
WebsiteGroupID: group.ID,
}
websiteService := NewIWebsiteService()
if err := websiteService.CreateWebsite(createWebsiteReq); err != nil {
return err
}
website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(strings.ToLower(req.Domain)))
if err != nil {
return err
}
_ = settingRepo.UpdateOrCreate("MCP_WEBSITE_ID", fmt.Sprintf("%d", website.ID))
if len(ipList) > 0 {
if err = ConfigAllowIPs(ipList, website); err != nil {
return err
}
}
if req.SSLID > 0 {
sslReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: true,
Type: "existed",
WebsiteSSLID: req.SSLID,
HttpConfig: "HTTPSOnly",
}
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
return err
}
}
if err = addMCPProxy(website.ID); err != nil {
return err
}
return nil
}
func (m McpServerService) UpdateBindDomain(req request.McpBindDomainUpdate) error {
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
if nginxInstall.ID == 0 {
return buserr.New("ErrOpenrestyInstall")
}
var (
ipList []string
err error
)
if len(req.IPList) > 0 {
ipList, err = common.HandleIPList(req.IPList)
if err != nil {
return err
}
}
if req.SSLID > 0 {
ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
websiteService := NewIWebsiteService()
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
return err
}
if err = ConfigAllowIPs(ipList, website); err != nil {
return err
}
if req.SSLID > 0 {
sslReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: true,
Type: "existed",
WebsiteSSLID: req.SSLID,
HttpConfig: "HTTPSOnly",
}
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
return err
}
}
if website.WebsiteSSLID > 0 && req.SSLID == 0 {
sslReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: false,
}
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
return err
}
}
go updateMcpConfig(website.ID)
return nil
}
func updateMcpConfig(websiteID uint) {
servers, _ := mcpServerRepo.List()
if len(servers) == 0 {
return
}
website, _ := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
websiteDomain := website.Domains[0]
var baseUrl string
if website.Protocol == constant.ProtocolHTTP {
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
} else {
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
}
go func() {
for _, server := range servers {
if server.BaseURL != baseUrl {
server.BaseURL = baseUrl
server.HostIP = "127.0.0.1"
_ = updateMcpServer(&server)
}
}
}()
}
func addProxy(server *model.McpServer) {
websiteID := GetWebsiteID()
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
if err != nil {
global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
return
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
return
}
fileOp := files.NewFileOp()
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
if !fileOp.Stat(includeDir) {
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
return
}
}
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
if err != nil {
return
}
includePath := path.Join(includeDir, server.Name+".conf")
config.FilePath = includePath
directives := config.Directives
location, ok := directives[0].(*components.Location)
if !ok {
return
}
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
location.ChangePath("^~", server.SsePath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
global.LOG.Errorf("write config failed, err: %v", buserr.WithErr(constant.ErrUpdateBuWebsite, err))
return
}
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
global.LOG.Errorf("update nginx config failed, err: %v", err)
return
}
}
func addMCPProxy(websiteID uint) error {
servers, _ := mcpServerRepo.List()
if len(servers) == 0 {
return nil
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
if err != nil {
return err
}
fileOp := files.NewFileOp()
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
if !fileOp.Stat(includeDir) {
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
return err
}
}
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
if err != nil {
return err
}
websiteDomain := website.Domains[0]
var baseUrl string
if website.Protocol == constant.ProtocolHTTP {
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
} else {
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
}
if websiteDomain.Port != 80 && websiteDomain.Port != 443 {
baseUrl = fmt.Sprintf("%s:%d", baseUrl, websiteDomain.Port)
}
for _, server := range servers {
includePath := path.Join(includeDir, server.Name+".conf")
config.FilePath = includePath
directives := config.Directives
location, ok := directives[0].(*components.Location)
if !ok {
err = errors.New("error")
return err
}
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
location.ChangePath("^~", server.SsePath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr(constant.ErrUpdateBuWebsite, err)
}
server.BaseURL = baseUrl
server.HostIP = "127.0.0.1"
go updateMcpServer(&server)
}
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
return err
}
return nil
}
func updateMcpServer(mcpServer *model.McpServer) error {
env := handleEnv(mcpServer)
if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
return err
}
_ = mcpServerRepo.Save(mcpServer)
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
if _, err := compose.Up(composePath); err != nil {
mcpServer.Status = constant.RuntimeError
mcpServer.Message = err.Error()
}
return mcpServerRepo.Save(mcpServer)
}
func handleEnv(mcpServer *model.McpServer) gotenv.Env {
env := make(gotenv.Env)
env["CONTAINER_NAME"] = mcpServer.ContainerName
env["COMMAND"] = mcpServer.Command
env["PANEL_APP_PORT_HTTP"] = strconv.Itoa(mcpServer.Port)
env["BASE_URL"] = mcpServer.BaseURL
env["SSE_PATH"] = mcpServer.SsePath
env["HOST_IP"] = mcpServer.HostIP
envStr, _ := gotenv.Marshal(env)
mcpServer.Env = envStr
return env
}
func handleCreateParams(mcpServer *model.McpServer, environments []request.Environment, volumes []request.Volume) error {
var composeContent []byte
if mcpServer.ID == 0 {
composeContent = mcp.DefaultMcpCompose
} else {
composeContent = []byte(mcpServer.DockerCompose)
}
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
return err
}
services, serviceValid := composeMap["services"].(map[string]interface{})
if !serviceValid {
return buserr.New(constant.ErrFileParse)
}
serviceName := ""
serviceValue := make(map[string]interface{})
if mcpServer.ID > 0 {
serviceName = mcpServer.Name
serviceValue = services[serviceName].(map[string]interface{})
} else {
for name, service := range services {
serviceName = name
serviceValue = service.(map[string]interface{})
break
}
delete(services, serviceName)
}
delete(serviceValue, "environment")
if len(environments) > 0 {
envMap := make(map[string]string)
for _, env := range environments {
envMap[env.Key] = env.Value
}
serviceValue["environment"] = envMap
}
delete(serviceValue, "volumes")
if len(volumes) > 0 {
volumeList := make([]string, 0)
for _, volume := range volumes {
volumeList = append(volumeList, fmt.Sprintf("%s:%s", volume.Source, volume.Target))
}
serviceValue["volumes"] = volumeList
}
services[mcpServer.Name] = serviceValue
composeByte, err := yaml.Marshal(composeMap)
if err != nil {
return err
}
mcpServer.DockerCompose = string(composeByte)
return nil
}
func startMcp(mcpServer *model.McpServer) {
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
if mcpServer.Status != constant.RuntimeNormal {
_, _ = compose.Down(composePath)
}
if out, err := compose.Up(composePath); err != nil {
mcpServer.Status = constant.RuntimeError
mcpServer.Message = out
} else {
mcpServer.Status = constant.RuntimeRunning
mcpServer.Message = ""
}
_ = syncMcpServerContainerStatus(mcpServer)
}
func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
containerNames := []string{mcpServer.ContainerName}
cli, err := docker.NewClient()
if err != nil {
return err
}
defer cli.Close()
containers, err := cli.ListContainersByName(containerNames)
if err != nil {
return err
}
if len(containers) == 0 {
mcpServer.Status = constant.RuntimeStopped
return mcpServerRepo.Save(mcpServer)
}
container := containers[0]
switch container.State {
case "exited":
mcpServer.Status = constant.RuntimeError
case "running":
mcpServer.Status = constant.RuntimeRunning
case "paused":
mcpServer.Status = constant.RuntimeStopped
case "restarting":
mcpServer.Status = constant.RuntimeReStarting
default:
if mcpServer.Status != constant.RuntimeBuildIng {
mcpServer.Status = constant.RuntimeStopped
}
}
return mcpServerRepo.Save(mcpServer)
}
func GetWebsiteID() uint {
websiteID, _ := settingRepo.Get(settingRepo.WithByKey("MCP_WEBSITE_ID"))
if websiteID.Value == "" {
return 0
}
websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64)
return uint(websiteIDUint)
}
+6 -6
View File
@@ -21,10 +21,10 @@ import (
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/encrypt" "github.com/1Panel-dev/1Panel/backend/utils/encrypt"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
) )
@@ -166,7 +166,7 @@ func (u *SettingService) UpdateBindInfo(req dto.BindInfo) error {
} }
go func() { go func() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system with new bind info failed, err: %v", err) global.LOG.Errorf("restart system with new bind info failed, err: %v", err)
} }
@@ -214,7 +214,7 @@ func (u *SettingService) UpdatePort(port uint) error {
} }
go func() { go func() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system port failed, err: %v", err) global.LOG.Errorf("restart system port failed, err: %v", err)
} }
@@ -237,7 +237,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
c.SetCookie(constant.SessionName, sID, 0, "", "", false, true) c.SetCookie(constant.SessionName, sID, 0, "", "", false, true)
go func() { go func() {
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system failed, err: %v", err) global.LOG.Errorf("restart system failed, err: %v", err)
} }
@@ -335,7 +335,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
c.SetCookie(constant.SessionName, sID, 0, "", "", true, true) c.SetCookie(constant.SessionName, sID, 0, "", "", true, true)
go func() { go func() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system failed, err: %v", err) global.LOG.Errorf("restart system failed, err: %v", err)
} }
@@ -552,7 +552,7 @@ func (u *SettingService) GenerateRSAKey() error {
return err return err
} }
privateKeyPEM := exportPrivateKeyToPEM(privateKey) privateKeyPEM := exportPrivateKeyToPEM(privateKey)
publicKeyPEM, err := exportPublicKeyToPEM(&privateKey.PublicKey) publicKeyPEM, _ := exportPublicKeyToPEM(&privateKey.PublicKey)
err = settingRepo.UpdateOrCreate("PASSWORD_PRIVATE_KEY", privateKeyPEM) err = settingRepo.UpdateOrCreate("PASSWORD_PRIVATE_KEY", privateKeyPEM)
if err != nil { if err != nil {
return err return err
+2 -1
View File
@@ -503,7 +503,8 @@ func hasOs(name string) bool {
strings.Contains(name, "arm64") || strings.Contains(name, "arm64") ||
strings.Contains(name, "armv7") || strings.Contains(name, "armv7") ||
strings.Contains(name, "ppc64le") || strings.Contains(name, "ppc64le") ||
strings.Contains(name, "s390x") strings.Contains(name, "s390x") ||
strings.Contains(name, "riscv64")
} }
func loadOs() string { func loadOs() string {
+31 -7
View File
@@ -17,6 +17,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
) )
type snapHelper struct { type snapHelper struct {
@@ -43,16 +44,39 @@ func snapPanel(snap snapHelper, targetDir string) {
defer snap.Wg.Done() defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running})
status := constant.StatusDone status := constant.StatusDone
if err := common.CopyFile("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil { h, err := systemctl.DefaultHandler("1panel")
status = err.Error()
if err != nil {
status = fmt.Sprintf("initialize service handle failed: %v", err)
snap.Status.Panel = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
return
} }
if err := common.CopyFile("/usr/local/bin/1pctl", targetDir); err != nil { binDir := systemctl.BinaryPath
servicePath, err := h.GetServicePath()
if err != nil {
status = fmt.Sprintf("get service path failed: %v", err)
snap.Status.Panel = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
return
}
if err := common.CopyFile(path.Join(binDir, "1panel"), path.Join(targetDir, "1panel")); err != nil {
status = err.Error() status = err.Error()
} }
_, _ = cmd.Execf("cp -r /usr/local/bin/lang %s", targetDir) if err := common.CopyFile(path.Join(binDir, "1pctl"), targetDir); err != nil {
status = err.Error()
if err := common.CopyFile("/etc/systemd/system/1panel.service", targetDir); err != nil { }
if _, err := cmd.Execf("cp -r %s/lang %s", binDir, targetDir); err != nil {
status = err.Error()
}
initScriptDir := path.Join(constant.DataDir, "initscript")
if err := common.CopyFile(servicePath, initScriptDir); err != nil {
status = err.Error()
}
global.LOG.Debugf("from %s copy init script to %s", initScriptDir, path.Join(targetDir, "initscript"))
if err := common.CopyDirs(initScriptDir, path.Join(targetDir, "initscript")); err != nil { // copy init script to targetDir
status = err.Error() status = err.Error()
} }
snap.Status.Panel = status snap.Status.Panel = status
@@ -260,7 +284,7 @@ func handleSnapTar(sourceDir, targetDir, name, exclusionRules string, secret str
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -print) -zcf %s %s %s %s", sourceDir, " -"+exStr, path, extraCmd, targetDir+"/"+name) commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -print) -zcf %s %s %s %s", sourceDir, " -"+exStr, path, extraCmd, targetDir+"/"+name)
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
} else { } else {
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%s' | sed 's|^|./|') -zcf %s %s -C %s .", sourceDir, "%P\n", targetDir+"/"+name, exStr, sourceDir) commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%%P\n' | sed 's|^|./|') -zcf %s %s -C %s .", sourceDir, targetDir+"/"+name, exStr, sourceDir)
global.LOG.Debug(commands) global.LOG.Debug(commands)
} }
stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute) stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute)
+34 -5
View File
@@ -15,6 +15,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@@ -91,8 +92,15 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
req.IsNew = true req.IsNew = true
} }
h, err := systemctl.DefaultHandler("1panel")
if err != nil {
updateRecoverStatus(snap.ID, isRecover, "ServiceHandle", constant.StatusFailed, fmt.Sprintf("initialize service handle failed: %v", err))
return
}
if req.IsNew || snap.InterruptStep == "1PanelBinary" { if req.IsNew || snap.InterruptStep == "1PanelBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), "/usr/local/bin"); err != nil { binDir := systemctl.BinaryPath
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), binDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error()) updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error())
return return
} }
@@ -100,16 +108,32 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
req.IsNew = true req.IsNew = true
} }
if req.IsNew || snap.InterruptStep == "1PctlBinary" { if req.IsNew || snap.InterruptStep == "1PctlBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), "/usr/local/bin"); err != nil { binDir := systemctl.BinaryPath
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), binDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error()) updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error())
return return
} }
_, _ = cmd.Execf("cp -r %s %s", path.Join(snapFileDir, "1panel/lang"), "/usr/local/bin/") langDir := path.Join(binDir, "lang")
if err := os.RemoveAll(langDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "RemoveLang", constant.StatusFailed, fmt.Sprintf("remove lang dir failed: %v", err))
return
}
if err := common.CopyDirs(path.Join(snapFileDir, "1panel/lang"), langDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "CopyLang", constant.StatusFailed, fmt.Sprintf("copy lang files failed: %v", err))
return
}
global.LOG.Debug("recover 1pctl from snapshot file successful!") global.LOG.Debug("recover 1pctl from snapshot file successful!")
req.IsNew = true req.IsNew = true
} }
if req.IsNew || snap.InterruptStep == "1PanelService" { if req.IsNew || snap.InterruptStep == "1PanelService" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel.service"), "/etc/systemd/system"); err != nil { servicePath, err := h.GetServicePath()
currentServiceName := h.GetServiceName()
if err != nil {
updateRecoverStatus(snap.ID, isRecover, "GetServicePath", constant.StatusFailed, fmt.Sprintf("get service path failed: %v", err))
return
}
global.LOG.Debugf("current service path: %s", servicePath)
if err := common.CopyFile(selectInitScript(path.Join(snapFileDir, "1panel/initscript"), currentServiceName), servicePath); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error()) updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error())
return return
} }
@@ -147,7 +171,12 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir)) global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir))
_ = os.RemoveAll(path.Dir(snapFileDir)) _ = os.RemoveAll(path.Dir(snapFileDir))
} }
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service") if h.ManagerName() == "systemd" {
_, _ = cmd.Exec("systemctl daemon-reload")
}
if err := systemctl.Restart("1panel"); err != nil {
global.LOG.Errorf("restart 1panel service failed: %v", err)
}
} }
func backupBeforeRecover(snap model.Snapshot) error { func backupBeforeRecover(snap model.Snapshot) error {
+214 -162
View File
@@ -2,8 +2,7 @@ package service
import ( import (
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/backend/utils/geo" "net"
"github.com/gin-gonic/gin"
"os" "os"
"os/user" "os/user"
"path" "path"
@@ -12,6 +11,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/1Panel-dev/1Panel/backend/utils/geo"
"github.com/gin-gonic/gin"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
@@ -59,26 +61,16 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
if err != nil { if err != nil {
data.Status = constant.StatusDisable data.Status = constant.StatusDisable
data.Message = err.Error() data.Message = err.Error()
} else {
active, err := systemctl.IsActive(serviceName)
if !active {
data.Status = constant.StatusDisable
if err != nil {
data.Message = err.Error()
}
} else {
data.Status = constant.StatusEnable
}
} }
isEnabled, _ := systemctl.IsEnable(serviceName)
out, err := systemctl.RunSystemCtl("is-enabled", serviceName) data.AutoStart = isEnabled
if err != nil { isActive, err := systemctl.IsActive(serviceName)
data.AutoStart = false if isActive {
data.Status = constant.StatusEnable
} else { } else {
if out == "alias\n" { data.Status = constant.StatusDisable
data.AutoStart, _ = systemctl.IsEnable("ssh") if err != nil {
} else { data.Message = err.Error()
data.AutoStart = out == "enabled\n"
} }
} }
@@ -121,29 +113,26 @@ func (u *SSHService) OperateSSH(operation string) error {
if err != nil { if err != nil {
return err return err
} }
sudo := cmd.SudoHandleCmd() switch operation {
if operation == "enable" || operation == "disable" { case "enable":
serviceName += ".service" return systemctl.Enable(serviceName)
} case "disable":
if operation == "stop" { return systemctl.Disable(serviceName)
case "start":
return systemctl.Start(serviceName)
case "stop":
isSocketActive, _ := systemctl.IsActive(serviceName + ".socket") isSocketActive, _ := systemctl.IsActive(serviceName + ".socket")
if isSocketActive { if isSocketActive {
std, err := cmd.Execf("%s systemctl stop %s", sudo, serviceName+".socket") if err := systemctl.Stop(serviceName + ".socket"); err != nil {
if err != nil { global.LOG.Errorf("Failed to stop %s.socket: %v", serviceName, err)
global.LOG.Errorf("handle systemctl stop %s.socket failed, err: %v", serviceName, std)
} }
} }
return systemctl.Stop(serviceName)
case "restart":
return systemctl.Restart(serviceName)
} }
if result, err := systemctl.CustomAction(operation, serviceName); err != nil {
stdout, err := cmd.Execf("%s systemctl %s %s", sudo, operation, serviceName) return fmt.Errorf("failed to execute custom action: %v", result.Output)
if err != nil {
if strings.Contains(stdout, "alias name or linked unit file") {
stdout, err := cmd.Execf("%s systemctl %s ssh", sudo, operation)
if err != nil {
return fmt.Errorf("%s ssh(alias name or linked unit file) failed, stdout: %s, err: %v", operation, stdout, err)
}
}
return fmt.Errorf("%s %s failed, stdout: %s, err: %v", operation, serviceName, stdout, err)
} }
return nil return nil
} }
@@ -198,7 +187,10 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
} }
} }
_, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName) err = systemctl.Restart(serviceName)
if err != nil {
global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err)
}
return nil return nil
} }
@@ -216,8 +208,10 @@ func (u *SSHService) UpdateByFile(value string) error {
if _, err = file.WriteString(value); err != nil { if _, err = file.WriteString(value); err != nil {
return err return err
} }
sudo := cmd.SudoHandleCmd() err = systemctl.Restart(serviceName)
_, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName) if err != nil {
global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err)
}
return nil return nil
} }
@@ -298,7 +292,7 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
if err != nil { if err != nil {
return err return err
} }
if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth")) { if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth") || strings.HasPrefix(info.Name(), "messages")) {
if !strings.HasSuffix(info.Name(), ".gz") { if !strings.HasSuffix(info.Name(), ".gz") {
fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()}) fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()})
return nil return nil
@@ -326,7 +320,8 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd()) nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
for _, file := range fileList { for _, file := range fileList {
commandItem := "" commandItem := ""
if strings.HasPrefix(path.Base(file.Name), "secure") { switch {
case strings.HasPrefix(path.Base(file.Name), "secure"):
switch req.Status { switch req.Status {
case constant.StatusSuccess: case constant.StatusSuccess:
commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command)
@@ -335,8 +330,16 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
default: default:
commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' %s", file.Name, command) commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' %s", file.Name, command)
} }
} case strings.HasPrefix(path.Base(file.Name), "messages"):
if strings.HasPrefix(path.Base(file.Name), "auth.log") { switch req.Status {
case constant.StatusSuccess:
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*Accepted (password|publickey)' %s", file.Name, command)
case constant.StatusFailed:
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Failed password for|Connection closed by authenticating user)' %s", file.Name, command)
default:
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Accepted|Failed password for|Connection closed)' %s", file.Name, command)
}
case strings.HasPrefix(path.Base(file.Name), "auth.log"):
switch req.Status { switch req.Status {
case constant.StatusSuccess: case constant.StatusSuccess:
commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command)
@@ -432,107 +435,160 @@ func loadSSHData(c *gin.Context, command string, showCountFrom, showCountTo, cur
lines := strings.Split(string(stdout2), "\n") lines := strings.Split(string(stdout2), "\n")
for i := len(lines) - 1; i >= 0; i-- { for i := len(lines) - 1; i >= 0; i-- {
var itemData dto.SSHHistory var itemData dto.SSHHistory
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 12 {
continue
}
// 统一时间解析逻辑
var dateStr string
var timeIndex int
if strings.Contains(parts[0], "-") { // 处理RFC3339时间格式
t, err := time.Parse(time.RFC3339Nano, parts[0])
if err == nil {
dateStr = t.Format("2006 Jan 2 15:04:05")
timeIndex = 0
}
} else { // 处理系统日志格式
dateParts := parts[:3]
if len(dateParts) < 3 {
continue
}
dateStr = strings.Join(dateParts, " ")
timeIndex = 3
}
// 根据日志类型解析内容
switch { switch {
case strings.Contains(lines[i], "Failed password for"): case strings.Contains(line, "Failed password for"):
itemData = loadFailedSecureDatas(lines[i]) itemData = parseFailedPasswordLog(parts, timeIndex, dateStr)
if len(itemData.Address) != 0 { case strings.Contains(line, "Connection closed by authenticating user"):
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { itemData = parseConnectionClosedLog(parts, timeIndex, dateStr)
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c)) case strings.Contains(line, "Accepted"):
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) itemData = parseAcceptedLog(parts, timeIndex, dateStr)
datas = append(datas, itemData) default:
} continue
failedCount++ }
} if itemData.Address == "" {
case strings.Contains(lines[i], "Connection closed by authenticating user"): continue
itemData = loadFailedAuthDatas(lines[i]) }
if len(itemData.Address) != 0 {
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { total := successCount + failedCount
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c)) if total >= showCountFrom && total < showCountTo {
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
datas = append(datas, itemData) itemData.Date = parseLogDate(currentYear, dateStr, nyc)
} datas = append(datas, itemData)
failedCount++ }
}
case strings.Contains(lines[i], "Accepted "): if itemData.Status == constant.StatusSuccess {
itemData = loadSuccessDatas(lines[i]) successCount++
if len(itemData.Address) != 0 { } else {
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { failedCount++
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c)) }
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
datas = append(datas, itemData) if total >= showCountTo {
} break
successCount++
}
} }
} }
return datas, successCount, failedCount return datas, successCount, failedCount
} }
func loadSuccessDatas(line string) dto.SSHHistory { func parseFailedPasswordLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
var data dto.SSHHistory data := dto.SSHHistory{
parts := strings.Fields(line) DateStr: dateStr,
index, dataStr := analyzeDateStr(parts) Status: constant.StatusFailed,
if dataStr == "" { AuthMode: "publickey",
return data
} }
data.DateStr = dataStr
data.AuthMode = parts[4+index]
data.User = parts[6+index]
data.Address = parts[8+index]
data.Port = parts[10+index]
data.Status = constant.StatusSuccess
return data
}
func loadFailedAuthDatas(line string) dto.SSHHistory { // 查找关键字段位置
var data dto.SSHHistory for i := timeIndex; i < len(parts); i++ {
parts := strings.Fields(line) switch parts[i] {
index, dataStr := analyzeDateStr(parts) case "for":
if dataStr == "" { if i+1 < len(parts) {
return data data.User = parts[i+1]
}
case "from":
if i+1 < len(parts) {
data.Address = parts[i+1]
}
case "port":
if i+1 < len(parts) {
data.Port = parts[i+1]
}
case "password":
data.AuthMode = "password"
}
} }
data.DateStr = dataStr
switch index { if strings.Contains(strings.Join(parts, " "), "invalid user") {
case 1: data.Message = "invalid user attempt"
data.User = parts[9]
case 2:
data.User = parts[10]
default:
data.User = parts[7]
}
data.AuthMode = parts[6+index]
data.Address = parts[9+index]
data.Port = parts[11+index]
data.Status = constant.StatusFailed
if strings.Contains(line, ": ") {
data.Message = strings.Split(line, ": ")[1]
}
return data
}
func loadFailedSecureDatas(line string) dto.SSHHistory {
var data dto.SSHHistory
parts := strings.Fields(line)
index, dataStr := analyzeDateStr(parts)
if dataStr == "" {
return data
}
data.DateStr = dataStr
if strings.Contains(line, " invalid ") {
data.AuthMode = parts[4+index]
index += 2
} else {
data.AuthMode = parts[4+index]
}
data.User = parts[6+index]
data.Address = parts[8+index]
data.Port = parts[10+index]
data.Status = constant.StatusFailed
if strings.Contains(line, ": ") {
data.Message = strings.Split(line, ": ")[1]
} }
return data return data
} }
func parseConnectionClosedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
data := dto.SSHHistory{
DateStr: dateStr,
Status: constant.StatusFailed,
}
// 解析Alpine格式的特殊字段
fieldStart := timeIndex + 5 // 跳过时间、主机、进程字段
for i := fieldStart; i < len(parts); i++ {
switch {
case parts[i] == "user":
if i+1 < len(parts) {
data.User = parts[i+1]
}
case parts[i] == "port":
if i+1 < len(parts) {
data.Port = parts[i+1]
}
case isIPAddress(parts[i]):
data.Address = parts[i]
}
}
return data
}
func parseAcceptedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
data := dto.SSHHistory{
DateStr: dateStr,
Status: constant.StatusSuccess,
AuthMode: "password", // 默认值
}
// 处理不同日志格式
fieldStart := timeIndex + 5 // 基础字段偏移
for i := fieldStart; i < len(parts); i++ {
switch {
case parts[i] == "for":
if i+1 < len(parts) {
data.User = parts[i+1]
}
case parts[i] == "from":
if i+1 < len(parts) {
data.Address = parts[i+1]
}
case parts[i] == "port":
if i+1 < len(parts) {
data.Port = parts[i+1]
}
case strings.Contains(parts[i], "ssh2:"):
data.AuthMode = "publickey"
case parts[i] == "publickey":
data.AuthMode = "publickey"
case parts[i] == "password":
data.AuthMode = "password"
}
}
return data
}
func handleGunzip(path string) error { func handleGunzip(path string) error {
if _, err := cmd.Execf("gunzip %s", path); err != nil { if _, err := cmd.Execf("gunzip %s", path); err != nil {
return err return err
@@ -541,40 +597,36 @@ func handleGunzip(path string) error {
} }
func loadServiceName() (string, error) { func loadServiceName() (string, error) {
if exist, _ := systemctl.IsExist("sshd"); exist { serviceName, err := systemctl.GetServiceName("ssh")
return "sshd", nil
} else if exist, _ := systemctl.IsExist("ssh"); exist {
return "ssh", nil
}
return "", errors.New("The ssh or sshd service is unavailable")
}
func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
itemDate, err := time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", currentYear, DateStr), nyc)
if err != nil { if err != nil {
itemDate, _ = time.ParseInLocation("2006 Jan 2 15:04:05", DateStr, nyc) if errors.Is(err, systemctl.ErrServiceNotFound) {
return "", fmt.Errorf("SSH service unavailable. Please ensure OpenSSH server is installed and configured")
}
return "", fmt.Errorf("failed to load SSH service name: %w", err)
} }
return itemDate return serviceName, nil
} }
func analyzeDateStr(parts []string) (int, string) { func parseLogDate(currentYear int, dateStr string, loc *time.Location) time.Time {
t, err := time.Parse(time.RFC3339Nano, parts[0]) // 处理带年份和不带年份的情况
if err == nil { formats := []string{
if len(parts) < 12 { "2006 Jan 2 15:04:05",
return 0, "" "Jan 2 15:04:05",
}
return 0, t.Format("2006 Jan 2 15:04:05")
}
t, err = time.Parse(constant.DateTimeLayout, fmt.Sprintf("%s %s", parts[0], parts[1]))
if err == nil {
if len(parts) < 14 {
return 0, ""
}
return 1, t.Format("2006 Jan 2 15:04:05")
} }
if len(parts) < 14 { for _, format := range formats {
return 0, "" t, err := time.ParseInLocation(format, dateStr, loc)
if err == nil {
if t.Year() == 0 {
return time.Date(currentYear, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, loc)
}
return t
}
} }
return 2, fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]) return time.Now().In(loc)
}
func isIPAddress(s string) bool {
return net.ParseIP(s) != nil
} }
+154 -80
View File
@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -17,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http" httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
) )
type UpgradeService struct{} type UpgradeService struct{}
@@ -103,123 +105,159 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
} }
downloadPath := fmt.Sprintf("%s/%s/%s/release", global.CONF.System.RepoUrl, mode, req.Version) downloadPath := fmt.Sprintf("%s/%s/%s/release", global.CONF.System.RepoUrl, mode, req.Version)
fileName := fmt.Sprintf("1panel-%s-%s-%s.tar.gz", req.Version, "linux", itemArch) fileName := fmt.Sprintf("1panel-%s-%s-%s.tar.gz", req.Version, "linux", itemArch)
_ = settingRepo.Update("SystemStatus", "Upgrading") serviceHandle, _ := systemctl.DefaultHandler("1panel")
currentServiceName := serviceHandle.GetServiceName()
if err := settingRepo.Update("SystemStatus", "Upgrading"); err != nil {
return fmt.Errorf("update system status failed: %w", err)
}
go func() { go func() {
defer func() {
if err := settingRepo.Update("SystemStatus", "Free"); err != nil {
global.LOG.Errorf("Reset system status failed: %v", err)
}
}()
_ = global.Cron.Stop() _ = global.Cron.Stop()
defer func() { defer global.Cron.Start()
global.Cron.Start()
}() if err := fileOp.DownloadFileWithProxy(
if err := fileOp.DownloadFileWithProxy(downloadPath+"/"+fileName, rootDir+"/"+fileName); err != nil { fmt.Sprintf("%s/%s", downloadPath, fileName),
global.LOG.Errorf("download service file failed, err: %v", err) path.Join(rootDir, fileName),
_ = settingRepo.Update("SystemStatus", "Free") ); err != nil {
global.LOG.Errorf("Failed to download upgrade package: %v", err)
return return
} }
global.LOG.Info("download all file successful!") defer os.RemoveAll(rootDir)
defer func() {
_ = os.Remove(rootDir) if err := handleUnTar(path.Join(rootDir, fileName), rootDir, ""); err != nil {
}() global.LOG.Errorf("Failed to extract package: %v", err)
if err := handleUnTar(rootDir+"/"+fileName, rootDir, ""); err != nil {
global.LOG.Errorf("decompress file failed, err: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
return return
} }
tmpDir := rootDir + "/" + strings.ReplaceAll(fileName, ".tar.gz", "")
tmpDir := path.Join(rootDir, strings.TrimSuffix(fileName, ".tar.gz"))
if err := u.handleBackup(fileOp, originalDir); err != nil { if err := u.handleBackup(fileOp, originalDir); err != nil {
global.LOG.Errorf("handle backup original file failed, err: %v", err) global.LOG.Errorf("Backup failed: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
return
}
global.LOG.Info("backup original data successful, now start to upgrade!")
if err := common.CopyFile(path.Join(tmpDir, "1panel"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("upgrade 1panel failed, err: %v", err)
u.handleRollback(originalDir, 1)
return return
} }
if err := common.CopyFile(path.Join(tmpDir, "1pctl"), "/usr/local/bin"); err != nil { binDir := systemctl.BinaryPath
global.LOG.Errorf("upgrade 1pctl failed, err: %v", err) servicePath, _ := serviceHandle.GetServicePath()
u.handleRollback(originalDir, 2) geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
return
}
_, _ = cmd.Execf("cp -r %s /usr/local/bin", path.Join(tmpDir, "lang"))
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
_, _ = cmd.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpDir, "GeoIP.mmdb"), geoPath)
if _, err := cmd.Execf("sed -i -e 's#BASE_DIR=.*#BASE_DIR=%s#g' /usr/local/bin/1pctl", global.CONF.System.BaseDir); err != nil { criticalUpdates := []struct {
global.LOG.Errorf("upgrade basedir in 1pctl failed, err: %v", err) src string
dest string
step int
}{
{path.Join(tmpDir, "1panel"), path.Join(binDir, "1panel"), 1},
{path.Join(tmpDir, "1pctl"), path.Join(binDir, "1pctl"), 2},
{selectInitScript(path.Join(tmpDir, "initscript"), currentServiceName), servicePath, 3},
}
for _, update := range criticalUpdates {
if err := common.Copy(update.src, update.dest); err != nil {
global.LOG.Errorf("Update %s failed: %v", path.Base(update.dest), err)
u.handleRollback(originalDir, update.step)
return
}
}
if _, err := cmd.Execf("sed -i -e 's#BASE_DIR=.*#BASE_DIR=%s#g' /usr/local/bin/1pctl",
global.CONF.System.BaseDir); err != nil {
global.LOG.Errorf("Update base directory failed: %v", err)
u.handleRollback(originalDir, 2) u.handleRollback(originalDir, 2)
return return
} }
if err := common.CopyFile(path.Join(tmpDir, "1panel.service"), "/etc/systemd/system"); err != nil { langDir := path.Join(binDir, "lang")
global.LOG.Errorf("upgrade 1panel.service failed, err: %v", err) if err := common.Copy(path.Join(tmpDir, "lang"), langDir); err != nil {
u.handleRollback(originalDir, 3) global.LOG.Errorf("Update language files failed: %v", err)
return }
if err := common.Copy(path.Join(tmpDir, "GeoIP.mmdb"), geoPath); err != nil {
global.LOG.Warnf("Update GeoIP database failed: %v", err)
} }
global.LOG.Info("upgrade successful!") global.LOG.Info("upgrade successful!")
go writeLogs(req.Version) go writeLogs(req.Version)
_ = settingRepo.Update("SystemVersion", req.Version)
_ = settingRepo.Update("SystemStatus", "Free")
checkPointOfWal() checkPointOfWal()
_, _ = cmd.ExecWithTimeOut("systemctl daemon-reload && systemctl restart 1panel.service", 1*time.Minute) if err := settingRepo.Update("SystemVersion", req.Version); err != nil {
global.LOG.Errorf("Update system version failed: %v", err)
}
if serviceHandle.ManagerName() == "systemd" {
_, _ = cmd.Exec("systemctl daemon-reload")
}
if err := systemctl.Restart("1panel"); err != nil {
global.LOG.Errorf("Service restart failed: %v", err)
return
}
}() }()
return nil return nil
} }
func (u *UpgradeService) handleBackup(fileOp files.FileOp, originalDir string) error { func (u *UpgradeService) handleBackup(fileOp files.FileOp, originalDir string) error {
if err := fileOp.Copy("/usr/local/bin/1panel", originalDir); err != nil { global.LOG.Info("Initiating backup procedure...")
return err h, _ := systemctl.DefaultHandler("1panel")
binDir := systemctl.BinaryPath
servicePath, _ := h.GetServicePath()
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
backupItems := []struct {
src string
dest string
}{
{path.Join(binDir, "1panel"), originalDir},
{path.Join(binDir, "1pctl"), originalDir},
{servicePath, originalDir},
{path.Join(binDir, "lang"), originalDir},
{geoPath, originalDir},
} }
if err := fileOp.Copy("/usr/local/bin/1pctl", originalDir); err != nil {
return err for _, item := range backupItems {
if err := fileOp.Copy(item.src, item.dest); err != nil {
return fmt.Errorf("backup %s failed: %w", path.Base(item.src), err)
}
} }
if err := fileOp.Copy("/etc/systemd/system/1panel.service", originalDir); err != nil {
return err if err := handleTar(
} path.Join(global.CONF.System.BaseDir, "1panel/db"),
_, _ = cmd.Execf("cp -r /usr/local/bin/lang %s", originalDir) originalDir,
_, _ = cmd.Execf("cp %s %s", path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb"), originalDir) "db.tar.gz",
checkPointOfWal() "db/1Panel.db-*",
if err := handleTar(path.Join(global.CONF.System.BaseDir, "1panel/db"), originalDir, "db.tar.gz", "db/1Panel.db-*", ""); err != nil { "",
return err ); err != nil {
return fmt.Errorf("database backup failed: %w", err)
} }
return nil return nil
} }
func (u *UpgradeService) handleRollback(originalDir string, errStep int) { func (u *UpgradeService) handleRollback(originalDir string, errStep int) {
_ = settingRepo.Update("SystemStatus", "Free") global.LOG.Info("Initiating rollback procedure...")
h, _ := systemctl.DefaultHandler("1panel")
binDir := systemctl.BinaryPath
servicePath, _ := h.GetServicePath()
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
checkPointOfWal() rollbackSteps := []struct {
if _, err := os.Stat(path.Join(originalDir, "1Panel.db")); err == nil { src string
if err := common.CopyFile(path.Join(originalDir, "1Panel.db"), global.CONF.System.DbPath); err != nil { dest string
global.LOG.Errorf("rollback 1panel db failed, err: %v", err) }{
{path.Join(originalDir, "1panel"), path.Join(binDir, "1panel")},
{path.Join(originalDir, "1pctl"), path.Join(binDir, "1pctl")},
{path.Join(originalDir, filepath.Base(servicePath)), servicePath},
{path.Join(originalDir, "lang"), path.Join(binDir, "lang")},
{path.Join(originalDir, "GeoIP.mmdb"), geoPath},
}
for _, step := range rollbackSteps[:errStep] {
if err := common.CopyFile(step.src, step.dest); err != nil {
global.LOG.Errorf("Rollback %s failed: %v", path.Base(step.src), err)
} }
} }
if _, err := os.Stat(path.Join(originalDir, "db.tar.gz")); err == nil {
if err := handleUnTar(path.Join(originalDir, "db.tar.gz"), global.CONF.System.DbPath, ""); err != nil {
global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
}
}
if err := common.CopyFile(path.Join(originalDir, "1panel"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("rollback 1pctl failed, err: %v", err)
}
if errStep == 1 {
return
}
if err := common.CopyFile(path.Join(originalDir, "1pctl"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("rollback 1panel failed, err: %v", err)
}
_, _ = cmd.Execf("cp -r %s /usr/local/bin", path.Join(originalDir, "lang"))
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
_, _ = cmd.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(originalDir, "GeoIP.mmdb"), geoPath)
if errStep == 2 { if err := systemctl.Restart("1panel"); err != nil {
return global.LOG.Errorf("Service restart during rollback failed: %v", err)
}
if err := common.CopyFile(path.Join(originalDir, "1panel.service"), "/etc/systemd/system"); err != nil {
global.LOG.Errorf("rollback 1panel failed, err: %v", err)
} }
} }
@@ -357,5 +395,41 @@ func loadArch() (string, error) {
if strings.Contains(std, "s390x") { if strings.Contains(std, "s390x") {
return "s390x", nil return "s390x", nil
} }
if strings.Contains(std, "riscv64") {
return "riscv64", nil
}
return "", fmt.Errorf("unsupported such arch: %s", std) return "", fmt.Errorf("unsupported such arch: %s", std)
} }
func selectInitScript(path string, serviceName string) string {
path = strings.TrimSuffix(path, "/")
mgr := systemctl.GetGlobalManager().Name()
var serviceFileName string
switch mgr {
case "systemd":
serviceFileName = "1panel.service"
case "openrc":
serviceFileName = "1paneld.openrc"
case "sysvinit":
isWrt := systemctl.FileExist("/etc/rc.common")
if isWrt {
serviceFileName = "1paneld.procd"
} else {
serviceFileName = "1paneld.init"
}
default:
serviceFileName = serviceName
global.LOG.Warnf("[%s]unselect InitScript, used default: %s", mgr, serviceName)
}
sourcePath := filepath.Join(path, serviceFileName)
targetPath := filepath.Join(path, serviceName)
if serviceFileName != serviceName {
if _, err := cmd.Execf("cp %s %s", sourcePath, targetPath); err != nil {
global.LOG.Errorf("Failed to copy init script from %s to %s: %v",
serviceFileName, serviceName, err)
}
}
return targetPath
}
+22 -5
View File
@@ -427,7 +427,9 @@ func (w WebsiteService) UpdateWebsite(req request.WebsiteUpdate) error {
} }
} }
website.PrimaryDomain = req.PrimaryDomain website.PrimaryDomain = req.PrimaryDomain
website.WebsiteGroupID = req.WebsiteGroupID if req.WebsiteGroupID > 0 {
website.WebsiteGroupID = req.WebsiteGroupID
}
website.Remark = req.Remark website.Remark = req.Remark
website.IPV6 = req.IPV6 website.IPV6 = req.IPV6
@@ -496,6 +498,10 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error {
if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil { if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil {
return err return err
} }
websiteID := GetWebsiteID()
if req.ID == websiteID {
_ = settingRepo.Update("MCP_WEBSITE_ID", "0")
}
tx.Commit() tx.Commit()
uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/website/%s", website.Alias)) uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/website/%s", website.Alias))
@@ -1764,11 +1770,22 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
} }
directives := config.GetDirectives() directives := config.GetDirectives()
location, ok := directives[0].(*components.Location) var (
if !ok { location *components.Location
err = errors.New("error") ok bool
return )
for _, directive := range directives {
if directive.GetName() == "location" {
location, ok = directive.(*components.Location)
if ok {
break
}
}
} }
if location == nil {
return nil, buserr.New("ErrConfigParse")
}
proxyConfig.ProxyPass = location.ProxyPass proxyConfig.ProxyPass = location.ProxyPass
proxyConfig.Cache = location.Cache proxyConfig.Cache = location.Cache
if location.CacheTime > 0 { if location.CacheTime > 0 {
+1
View File
@@ -35,6 +35,7 @@ var WebUrlMap = map[string]struct{}{
"/ai": {}, "/ai": {},
"/ai/model": {}, "/ai/model": {},
"/ai/gpu": {}, "/ai/gpu": {},
"/ai/mcp": {},
"/containers": {}, "/containers": {},
"/containers/container": {}, "/containers/container": {},
+1
View File
@@ -17,4 +17,5 @@ var (
RuntimeDir = path.Join(DataDir, "runtime") RuntimeDir = path.Join(DataDir, "runtime")
RecycleBinDir = "/.1panel_clash" RecycleBinDir = "/.1panel_clash"
SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl") SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl")
McpDir = path.Join(DataDir, "mcp")
) )
+1
View File
@@ -13,6 +13,7 @@ const (
RuntimeStopped = "stopped" RuntimeStopped = "stopped"
RuntimeUnhealthy = "unhealthy" RuntimeUnhealthy = "unhealthy"
RuntimeCreating = "creating" RuntimeCreating = "creating"
RuntimeReStarting = "restarting"
RuntimePHP = "php" RuntimePHP = "php"
RuntimeNode = "node" RuntimeNode = "node"
+3
View File
@@ -2,6 +2,7 @@ package global
import ( import (
"github.com/1Panel-dev/1Panel/backend/configs" "github.com/1Panel-dev/1Panel/backend/configs"
"github.com/1Panel-dev/1Panel/backend/init/auth"
"github.com/1Panel-dev/1Panel/backend/init/cache/badger_db" "github.com/1Panel-dev/1Panel/backend/init/cache/badger_db"
"github.com/1Panel-dev/1Panel/backend/init/session/psession" "github.com/1Panel-dev/1Panel/backend/init/session/psession"
"github.com/dgraph-io/badger/v4" "github.com/dgraph-io/badger/v4"
@@ -28,6 +29,8 @@ var (
MonitorCronID cron.EntryID MonitorCronID cron.EntryID
OneDriveCronID cron.EntryID OneDriveCronID cron.EntryID
IPTracker *auth.IPTracker
I18n *i18n.Localizer I18n *i18n.Localizer
I18nForCmd *i18n.Localizer I18nForCmd *i18n.Localizer
) )
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "mode: "
#ai-tool #ai-tool
ErrOpenrestyInstall: 'Please install Openresty first' ErrOpenrestyInstall: 'Please install Openresty first'
ErrSSL: "Certificate content is empty, please check the certificate!" ErrSSL: "Certificate content is empty, please check the certificate!"
ErrSsePath: "SSE path is duplicated"
#mobile app #mobile app
ErrVerifyToken: 'Token verification error, please reset and scan again.' ErrVerifyToken: 'Token verification error, please reset and scan again.'
+1
View File
@@ -285,6 +285,7 @@ SystemMode: "モード: "
#ai-tool #ai-tool
ErrOpenrestyInstall: 'まず Openresty をインストールしてください' ErrOpenrestyInstall: 'まず Openresty をインストールしてください'
ErrSSL: "証明書の内容が空です。証明書を確認してください!" ErrSSL: "証明書の内容が空です。証明書を確認してください!"
ErrSsePath: "SSE パスが重複しています"
#mobile app #mobile app
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "모드: "
#ai-tool #ai-tool
ErrOpenrestyInstall: '먼저 Openresty를 설치하세요' ErrOpenrestyInstall: '먼저 Openresty를 설치하세요'
ErrSSL: "인증서 내용이 비어 있습니다. 인증서를 확인하세요!" ErrSSL: "인증서 내용이 비어 있습니다. 인증서를 확인하세요!"
ErrSsePath: "SSE 경로가 중복되었습니다"
#mobile app #mobile app
+1 -1
View File
@@ -287,7 +287,7 @@ SystemMode: "Mod: "
#ai-tool #ai-tool
ErrOpenrestyInstall: 'Sila pasang Openresty terlebih dahulu' ErrOpenrestyInstall: 'Sila pasang Openresty terlebih dahulu'
ErrSSL: "Kandungan sijil kosong, sila periksa sijil!" ErrSSL: "Kandungan sijil kosong, sila periksa sijil!"
ErrSsePath: "Laluan SSE bertindan"
#mobile app #mobile app
ErrVerifyToken: 'Ralat pengesahan token, sila tetapkan semula dan imbas semula.' ErrVerifyToken: 'Ralat pengesahan token, sila tetapkan semula dan imbas semula.'
+1
View File
@@ -285,6 +285,7 @@ SystemMode: "modo: "
#ai-tool #ai-tool
ErrOpenrestyInstall: 'Por favor, instale o Openresty primeiro' ErrOpenrestyInstall: 'Por favor, instale o Openresty primeiro'
ErrSSL: "O conteúdo do certificado está vazio, por favor, verifique o certificado!" ErrSSL: "O conteúdo do certificado está vazio, por favor, verifique o certificado!"
ErrSsePath: "Caminho SSE duplicado"
#mobile app #mobile app
ErrVerifyToken: 'Erro de verificação do token, por favor, reinicie e escaneie novamente.' ErrVerifyToken: 'Erro de verificação do token, por favor, reinicie e escaneie novamente.'
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "режим: "
#ai-tool #ai-tool
ErrOpenrestyInstall: "Пожалуйста, установите Openresty сначала" ErrOpenrestyInstall: "Пожалуйста, установите Openresty сначала"
ErrSSL: "Содержимое сертификата пустое, пожалуйста, проверьте сертификат!" ErrSSL: "Содержимое сертификата пустое, пожалуйста, проверьте сертификат!"
ErrSsePath: "Путь SSE дублируется"
#mobile app #mobile app
ErrVerifyToken: 'шибка проверки токена, пожалуйста, сбросьте и отсканируйте снова.' ErrVerifyToken: 'шибка проверки токена, пожалуйста, сбросьте и отсканируйте снова.'
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "模式: "
#ai-tool #ai-tool
ErrOpenrestyInstall: "請先安裝 Openresty" ErrOpenrestyInstall: "請先安裝 Openresty"
ErrSSL: "證書內容為空,請檢查證書!" ErrSSL: "證書內容為空,請檢查證書!"
ErrSsePath: "SSE 路徑重複"
#mobile app #mobile app
ErrVerifyToken: '令牌驗證錯誤,請重置後再次掃碼' ErrVerifyToken: '令牌驗證錯誤,請重置後再次掃碼'
+1
View File
@@ -288,6 +288,7 @@ SystemMode: "模式: "
#ai-tool #ai-tool
ErrOpenrestyInstall: '请先安装 Openresty' ErrOpenrestyInstall: '请先安装 Openresty'
ErrSSL: "证书内容为空,请检查证书!" ErrSSL: "证书内容为空,请检查证书!"
ErrSsePath: "SSE 路径重复"
#mobile app #mobile app
ErrVerifyToken: '令牌验证错误,请重置后再次扫码' ErrVerifyToken: '令牌验证错误,请重置后再次扫码'
+4 -1
View File
@@ -23,8 +23,11 @@ func Init() {
constant.SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl") constant.SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl")
constant.McpDir = path.Join(constant.DataDir, "mcp")
dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir, dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir,
global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir, constant.SSLLogDir} global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir,
constant.SSLLogDir, constant.McpDir}
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
for _, dir := range dirs { for _, dir := range dirs {
+99
View File
@@ -0,0 +1,99 @@
package auth
import (
"sync"
"time"
)
const (
MaxIPCount = 100
ExpireDuration = 30 * time.Minute
)
type IPRecord struct {
NeedCaptcha bool
LastUpdate time.Time
}
type IPTracker struct {
records map[string]*IPRecord
ipOrder []string
mu sync.RWMutex
}
func NewIPTracker() *IPTracker {
return &IPTracker{
records: make(map[string]*IPRecord),
ipOrder: make([]string, 0),
}
}
func (t *IPTracker) NeedCaptcha(ip string) bool {
t.mu.Lock()
defer t.mu.Unlock()
record, exists := t.records[ip]
if !exists {
return false
}
if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
return false
}
return record.NeedCaptcha
}
func (t *IPTracker) SetNeedCaptcha(ip string) {
t.mu.Lock()
defer t.mu.Unlock()
if record, exists := t.records[ip]; exists {
if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
} else {
record.NeedCaptcha = true
record.LastUpdate = time.Now()
return
}
}
if len(t.records) >= MaxIPCount {
t.removeOldestUnsafe()
}
t.records[ip] = &IPRecord{
NeedCaptcha: true,
LastUpdate: time.Now(),
}
t.ipOrder = append(t.ipOrder, ip)
}
func (t *IPTracker) Clear(ip string) {
t.mu.Lock()
defer t.mu.Unlock()
t.removeIPUnsafe(ip)
}
func (t *IPTracker) removeIPUnsafe(ip string) {
delete(t.records, ip)
for i, storedIP := range t.ipOrder {
if storedIP == ip {
t.ipOrder = append(t.ipOrder[:i], t.ipOrder[i+1:]...)
break
}
}
}
func (t *IPTracker) removeOldestUnsafe() {
if len(t.ipOrder) == 0 {
return
}
oldestIP := t.ipOrder[0]
delete(t.records, oldestIP)
t.ipOrder = t.ipOrder[1:]
}
+5
View File
@@ -106,6 +106,11 @@ func Init() {
migrations.AddAppMenu, migrations.AddAppMenu,
migrations.AddAppPanelName, migrations.AddAppPanelName,
migrations.AddLicenseVerify, migrations.AddLicenseVerify,
migrations.AddMcpServer,
migrations.AddPbootCMSPHPExtensions,
migrations.DeleteV2Openresty,
migrations.UpdateOnedrive,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)
@@ -459,3 +459,53 @@ var AddLicenseVerify = &gormigrate.Migration{
return nil return nil
}, },
} }
var AddMcpServer = &gormigrate.Migration{
ID: "20250401-add-mcp-server",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.McpServer{}); err != nil {
return err
}
return nil
},
}
var AddPbootCMSPHPExtensions = &gormigrate.Migration{
ID: "20250625-add-php-extensions",
Migrate: func(tx *gorm.DB) error {
item := &model.PHPExtensions{
Name: "PbootCMS",
}
if err := tx.Where("name = ?", "PbootCMS").FirstOrCreate(item, &model.PHPExtensions{
Name: "PbootCMS",
Extensions: "curl,gd,mbstring,mysqli,openssl,pdo_mysql,pdo_sqlite,sqlite3",
}).Error; err != nil {
return err
}
return nil
},
}
var DeleteV2Openresty = &gormigrate.Migration{
ID: "20250701-delete-v2-openresty",
Migrate: func(tx *gorm.DB) error {
return tx.Where("version = '1.27.1.2-0-1-focal'").Delete(&model.AppDetail{}).Error
},
}
var UpdateOnedrive = &gormigrate.Migration{
ID: "20250704-update-onedrive",
Migrate: func(tx *gorm.DB) error {
if err := tx.Model(&model.Setting{}).
Where("key = ?", "OneDriveID").
Update("value", "NTQ0NmNmZTMtNGM3OS00N2EwLWFlMjUtZmM2NDU0NzhlMmQ5").Error; err != nil {
return err
}
if err := tx.Model(&model.Setting{}).
Where("key = ?", "OneDriveSc").
Update("value", "bGRlOFF+WEVrR1M0b25Vb1VsRWpMYzE2MW9rTXZEM25KdnZ1MGN6MA==").Error; err != nil {
return err
}
return nil
},
}
+5 -4
View File
@@ -3,14 +3,15 @@ package router
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/cmd/server/res"
"net/http" "net/http"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/cmd/server/res"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/i18n" "github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/middleware" "github.com/1Panel-dev/1Panel/backend/middleware"
@@ -160,7 +161,7 @@ func setWebStatic(rootRouter *gin.RouterGroup) {
} }
func Routers() *gin.Engine { func Routers() *gin.Engine {
Router = gin.Default() Router = gin.New()
Router.Use(middleware.OperationLog()) Router.Use(middleware.OperationLog())
// Router.Use(middleware.CSRF()) // Router.Use(middleware.CSRF())
// Router.Use(middleware.LoadCsrfToken()) // Router.Use(middleware.LoadCsrfToken())
+6 -1
View File
@@ -8,11 +8,17 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/repo" "github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func WhiteAllow() gin.HandlerFunc { func WhiteAllow() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
clientIP := common.GetRealClientIP(c)
if common.IsPrivateIP(clientIP) {
c.Next()
return
}
settingRepo := repo.NewISettingRepo() settingRepo := repo.NewISettingRepo()
status, err := settingRepo.Get(settingRepo.WithByKey("AllowIPs")) status, err := settingRepo.Get(settingRepo.WithByKey("AllowIPs"))
if err != nil { if err != nil {
@@ -24,7 +30,6 @@ func WhiteAllow() gin.HandlerFunc {
c.Next() c.Next()
return return
} }
clientIP := c.ClientIP()
for _, ip := range strings.Split(status.Value, ",") { for _, ip := range strings.Split(status.Value, ",") {
if len(ip) == 0 { if len(ip) == 0 {
continue continue
+1
View File
@@ -24,5 +24,6 @@ func commonGroups() []CommonRouter {
&ProcessRouter{}, &ProcessRouter{},
&WebsiteCARouter{}, &WebsiteCARouter{},
&AIToolsRouter{}, &AIToolsRouter{},
&McpServerRouter{},
} }
} }
+9
View File
@@ -26,5 +26,14 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/domain/bind", baseApi.BindDomain) aiToolsRouter.POST("/domain/bind", baseApi.BindDomain)
aiToolsRouter.POST("/domain/get", baseApi.GetBindDomain) aiToolsRouter.POST("/domain/get", baseApi.GetBindDomain)
aiToolsRouter.POST("/domain/update", baseApi.UpdateBindDomain) aiToolsRouter.POST("/domain/update", baseApi.UpdateBindDomain)
aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers)
aiToolsRouter.POST("/mcp/server", baseApi.CreateMcpServer)
aiToolsRouter.POST("/mcp/server/update", baseApi.UpdateMcpServer)
aiToolsRouter.POST("/mcp/server/del", baseApi.DeleteMcpServer)
aiToolsRouter.POST("/mcp/server/op", baseApi.OperateMcpServer)
aiToolsRouter.POST("/mcp/domain/bind", baseApi.BindMcpDomain)
aiToolsRouter.GET("/mcp/domain/get", baseApi.GetMcpBindDomain)
aiToolsRouter.POST("/mcp/domain/update", baseApi.UpdateMcpBindDomain)
} }
} }
+1 -1
View File
@@ -16,7 +16,7 @@ func (s *BaseRouter) InitRouter(Router *gin.RouterGroup) {
baseRouter.POST("/login", baseApi.Login) baseRouter.POST("/login", baseApi.Login)
baseRouter.POST("/logout", baseApi.LogOut) baseRouter.POST("/logout", baseApi.LogOut)
baseRouter.GET("/demo", baseApi.CheckIsDemo) baseRouter.GET("/demo", baseApi.CheckIsDemo)
baseRouter.GET("/language", baseApi.GetLanguage) baseRouter.GET("/setting", baseApi.GetAuthSetting)
baseRouter.GET("/intl", baseApi.CheckIsIntl) baseRouter.GET("/intl", baseApi.CheckIsIntl)
} }
} }
+21
View File
@@ -0,0 +1,21 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
"github.com/1Panel-dev/1Panel/backend/middleware"
"github.com/gin-gonic/gin"
)
type McpServerRouter struct {
}
func (m *McpServerRouter) InitRouter(Router *gin.RouterGroup) {
mcpRouter := Router.Group("mcp")
mcpRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
mcpRouter.POST("/search", baseApi.PageMcpServers)
mcpRouter.POST("/server", baseApi.CreateMcpServer)
}
}
+2
View File
@@ -13,6 +13,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/i18n" "github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/init/app" "github.com/1Panel-dev/1Panel/backend/init/app"
"github.com/1Panel-dev/1Panel/backend/init/auth"
"github.com/1Panel-dev/1Panel/backend/init/business" "github.com/1Panel-dev/1Panel/backend/init/business"
"github.com/1Panel-dev/1Panel/backend/init/lang" "github.com/1Panel-dev/1Panel/backend/init/lang"
@@ -52,6 +53,7 @@ func Start() {
business.Init() business.Init()
rootRouter := router.Routers() rootRouter := router.Routers()
global.IPTracker = auth.NewIPTracker()
tcpItem := "tcp4" tcpItem := "tcp4"
if global.CONF.System.Ipv6 == "enable" { if global.CONF.System.Ipv6 == "enable" {
+3 -3
View File
@@ -11,13 +11,13 @@ import (
var store = base64Captcha.DefaultMemStore var store = base64Captcha.DefaultMemStore
func VerifyCode(codeID string, code string) error { func VerifyCode(codeID string, code string) error {
if codeID == "" {
return constant.ErrCaptchaCode
}
vv := store.Get(codeID, true) vv := store.Get(codeID, true)
vv = strings.TrimSpace(vv) vv = strings.TrimSpace(vv)
code = strings.TrimSpace(code) code = strings.TrimSpace(code)
if codeID == "" || code == "" {
return constant.ErrCaptchaCode
}
if strings.EqualFold(vv, code) { if strings.EqualFold(vv, code) {
return nil return nil
} }
+73 -2
View File
@@ -3,13 +3,12 @@ package common
import ( import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/gin-gonic/gin"
"io" "io"
mathRand "math/rand" mathRand "math/rand"
"net" "net"
"os" "os"
"path" "path"
"path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"sort" "sort"
@@ -18,6 +17,9 @@ import (
"time" "time"
"unicode" "unicode"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/gin-gonic/gin"
"golang.org/x/net/idna" "golang.org/x/net/idna"
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
@@ -124,6 +126,9 @@ func GetSortedVersions(versions []string) []string {
} }
func CopyFile(src, dst string) error { func CopyFile(src, dst string) error {
if fi, err := os.Stat(src); err == nil && fi.IsDir() {
return fmt.Errorf("src is a directory, use CopyDirs instead")
}
source, err := os.Open(src) source, err := os.Open(src)
if err != nil { if err != nil {
return err return err
@@ -152,7 +157,57 @@ func CopyFile(src, dst string) error {
} }
return nil return nil
} }
func CopyDirs(srcDir, dstDir string) error {
srcInfo, err := os.Stat(srcDir)
if err != nil {
return err
}
if !srcInfo.IsDir() {
return fmt.Errorf("%s is not a directory", srcDir)
}
if err := os.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
return err
}
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
dstPath := filepath.Join(dstDir, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
if info.Mode()&os.ModeSymlink != 0 {
return copySymlink(path, dstPath)
}
return CopyFile(path, dstPath)
})
}
func copySymlink(src, dst string) error {
link, err := os.Readlink(src)
if err != nil {
return err
}
return os.Symlink(link, dst)
}
func Copy(src, dst string) error {
fi, err := os.Stat(src)
if err != nil {
return err
}
switch mode := fi.Mode(); {
case mode.IsDir():
return CopyDirs(src, dst)
case mode.IsRegular():
return CopyFile(src, dst)
default:
return fmt.Errorf("unsupported file type: %s", mode)
}
}
func IsCrossVersion(version1, version2 string) bool { func IsCrossVersion(version1, version2 string) bool {
version1s := strings.Split(version1, ".") version1s := strings.Split(version1, ".")
version2s := strings.Split(version2, ".") version2s := strings.Split(version2, ".")
@@ -371,3 +426,19 @@ func HandleIPList(content string) ([]string, error) {
} }
return res, nil return res, nil
} }
func GetRealClientIP(c *gin.Context) string {
addr := c.Request.RemoteAddr
if ip, _, err := net.SplitHostPort(addr); err == nil {
return ip
}
return addr
}
func IsPrivateIP(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback()
}
+20 -16
View File
@@ -349,26 +349,30 @@ func (f FileOp) DownloadFileWithProxy(url, dst string) error {
} }
func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error { func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
for _, p := range oldPaths { if len(oldPaths) == 0 {
var dstPath string return nil
if name != "" { }
dstPath = filepath.Join(dst, name) var dstPath string
if f.Stat(dstPath) { coverFlag := ""
dstPath = dst if name != "" {
} dstPath = filepath.Join(dst, name)
} else { if f.Stat(dstPath) {
base := filepath.Base(p) dstPath = dst
dstPath = filepath.Join(dst, base)
} }
coverFlag := ""
if cover { if cover {
coverFlag = "-f" coverFlag = "-f"
} }
} else {
cmdStr := fmt.Sprintf(`mv %s '%s' '%s'`, coverFlag, p, dstPath) dstPath = dst
if err := cmd.ExecCmd(cmdStr); err != nil { coverFlag = "-f"
return err }
} var quotedPaths []string
for _, p := range oldPaths {
quotedPaths = append(quotedPaths, fmt.Sprintf("'%s'", p))
}
mvCommand := fmt.Sprintf("mv %s %s '%s'", coverFlag, strings.Join(quotedPaths, " "), dstPath)
if err := cmd.ExecCmd(mvCommand); err != nil {
return err
} }
return nil return nil
} }
+4 -15
View File
@@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
) )
var ForwardListRegex = regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`) var ForwardListRegex = regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`)
@@ -41,27 +42,15 @@ func (f *Firewall) Version() (string, error) {
} }
func (f *Firewall) Start() error { func (f *Firewall) Start() error {
stdout, err := cmd.Exec("systemctl start firewalld") return systemctl.Start("firewalld")
if err != nil {
return fmt.Errorf("enable the firewall failed, err: %s", stdout)
}
return nil
} }
func (f *Firewall) Stop() error { func (f *Firewall) Stop() error {
stdout, err := cmd.Exec("systemctl stop firewalld") return systemctl.Stop("firewalld")
if err != nil {
return fmt.Errorf("stop the firewall failed, err: %s", stdout)
}
return nil
} }
func (f *Firewall) Restart() error { func (f *Firewall) Restart() error {
stdout, err := cmd.Exec("systemctl restart firewalld") return systemctl.Restart("firewalld")
if err != nil {
return fmt.Errorf("restart the firewall failed, err: %s", stdout)
}
return nil
} }
func (f *Firewall) Reload() error { func (f *Firewall) Reload() error {
+3 -1
View File
@@ -46,7 +46,9 @@ func NewLocation(directive IDirective) *Location {
location.Host = params[1] location.Host = params[1]
} }
case "proxy_cache": case "proxy_cache":
location.Cache = true if params[0] != "off" {
location.Cache = true
}
case "if": case "if":
if params[0] == "(" && params[1] == "$uri" && params[2] == "~*" { if params[0] == "(" && params[1] == "$uri" && params[2] == "~*" {
dirs := dir.GetBlock().GetDirectives() dirs := dir.GetBlock().GetDirectives()
+49 -15
View File
@@ -7,9 +7,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/go-acme/lego/v4/providers/dns/freemyip"
"github.com/go-acme/lego/v4/providers/dns/rainyun"
"github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/acme/api"
@@ -20,14 +17,19 @@ import (
"github.com/go-acme/lego/v4/providers/dns/alidns" "github.com/go-acme/lego/v4/providers/dns/alidns"
"github.com/go-acme/lego/v4/providers/dns/clouddns" "github.com/go-acme/lego/v4/providers/dns/clouddns"
"github.com/go-acme/lego/v4/providers/dns/cloudflare" "github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/providers/dns/cloudns"
"github.com/go-acme/lego/v4/providers/dns/dnspod" "github.com/go-acme/lego/v4/providers/dns/dnspod"
"github.com/go-acme/lego/v4/providers/dns/freemyip"
"github.com/go-acme/lego/v4/providers/dns/godaddy" "github.com/go-acme/lego/v4/providers/dns/godaddy"
"github.com/go-acme/lego/v4/providers/dns/huaweicloud" "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
"github.com/go-acme/lego/v4/providers/dns/namecheap" "github.com/go-acme/lego/v4/providers/dns/namecheap"
"github.com/go-acme/lego/v4/providers/dns/namedotcom" "github.com/go-acme/lego/v4/providers/dns/namedotcom"
"github.com/go-acme/lego/v4/providers/dns/namesilo" "github.com/go-acme/lego/v4/providers/dns/namesilo"
"github.com/go-acme/lego/v4/providers/dns/porkbun"
"github.com/go-acme/lego/v4/providers/dns/rainyun"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud" "github.com/go-acme/lego/v4/providers/dns/tencentcloud"
"github.com/go-acme/lego/v4/providers/dns/volcengine" "github.com/go-acme/lego/v4/providers/dns/volcengine"
"github.com/go-acme/lego/v4/providers/dns/westcn"
"github.com/go-acme/lego/v4/providers/http/webroot" "github.com/go-acme/lego/v4/providers/http/webroot"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -75,6 +77,7 @@ const (
Volcengine DnsType = "Volcengine" Volcengine DnsType = "Volcengine"
CloudFlare DnsType = "CloudFlare" CloudFlare DnsType = "CloudFlare"
CloudDns DnsType = "CloudDns" CloudDns DnsType = "CloudDns"
ClouDNS DnsType = "ClouDNS"
FreeMyIP DnsType = "FreeMyIP" FreeMyIP DnsType = "FreeMyIP"
NameSilo DnsType = "NameSilo" NameSilo DnsType = "NameSilo"
NameCheap DnsType = "NameCheap" NameCheap DnsType = "NameCheap"
@@ -83,21 +86,27 @@ const (
TencentCloud DnsType = "TencentCloud" TencentCloud DnsType = "TencentCloud"
HuaweiCloud DnsType = "HuaweiCloud" HuaweiCloud DnsType = "HuaweiCloud"
RainYun DnsType = "RainYun" RainYun DnsType = "RainYun"
WestCN DnsType = "WestCN"
PorkBun DnsType = "PorkBun"
) )
type DNSParam struct { type DNSParam struct {
ID string `json:"id"` ID string `json:"id"`
Token string `json:"token"` Token string `json:"token"`
AccessKey string `json:"accessKey"` AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"` SecretKey string `json:"secretKey"`
Email string `json:"email"` Email string `json:"email"`
APIkey string `json:"apiKey"` APIkey string `json:"apiKey"`
APIUser string `json:"apiUser"` APIUser string `json:"apiUser"`
APISecret string `json:"apiSecret"` APISecret string `json:"apiSecret"`
SecretID string `json:"secretID"` SecretID string `json:"secretID"`
Region string `json:"region"` Region string `json:"region"`
ClientID string `json:"clientID"` ClientID string `json:"clientID"`
Password string `json:"password"` Password string `json:"password"`
AuthID string `json:"authID"`
SubAuthID string `json:"subAuthID"`
AuthPassword string `json:"authPassword"`
Username string `json:"username"`
} }
var ( var (
@@ -158,6 +167,15 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
clouddnsConfig.PollingInterval = pollingInterval clouddnsConfig.PollingInterval = pollingInterval
clouddnsConfig.TTL = ttl clouddnsConfig.TTL = ttl
p, err = clouddns.NewDNSProviderConfig(clouddnsConfig) p, err = clouddns.NewDNSProviderConfig(clouddnsConfig)
case ClouDNS:
cloudnsConfig := cloudns.NewDefaultConfig()
cloudnsConfig.AuthID = param.AuthID
cloudnsConfig.SubAuthID = param.SubAuthID
cloudnsConfig.AuthPassword = param.AuthPassword
cloudnsConfig.PropagationTimeout = propagationTimeout
cloudnsConfig.PollingInterval = pollingInterval
cloudnsConfig.TTL = ttl
p, err = cloudns.NewDNSProviderConfig(cloudnsConfig)
case FreeMyIP: case FreeMyIP:
freeMyIpConfig := freemyip.NewDefaultConfig() freeMyIpConfig := freemyip.NewDefaultConfig()
freeMyIpConfig.Token = param.Token freeMyIpConfig.Token = param.Token
@@ -219,6 +237,22 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
rainyunConfig.PollingInterval = pollingInterval rainyunConfig.PollingInterval = pollingInterval
rainyunConfig.TTL = ttl rainyunConfig.TTL = ttl
p, err = rainyun.NewDNSProviderConfig(rainyunConfig) p, err = rainyun.NewDNSProviderConfig(rainyunConfig)
case WestCN:
westcnConfig := westcn.NewDefaultConfig()
westcnConfig.Username = param.Username
westcnConfig.Password = param.Password
westcnConfig.PropagationTimeout = propagationTimeout
westcnConfig.PollingInterval = pollingInterval
westcnConfig.TTL = ttl
p, err = westcn.NewDNSProviderConfig(westcnConfig)
case PorkBun:
porkbunConfig := porkbun.NewDefaultConfig()
porkbunConfig.APIKey = param.APIkey
porkbunConfig.SecretAPIKey = param.SecretKey
porkbunConfig.PropagationTimeout = propagationTimeout
porkbunConfig.PollingInterval = pollingInterval
porkbunConfig.TTL = ttl
p, err = porkbun.NewDNSProviderConfig(porkbunConfig)
} }
if err != nil { if err != nil {
return err return err
+393
View File
@@ -0,0 +1,393 @@
package systemctl
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"go4.org/syncutil/singleflight"
"golang.org/x/sync/errgroup"
)
var (
aliasFile string
serviceAliases sync.Map
saveTimer *time.Timer
saveMutex sync.Mutex
afterSaveTime = 20 * time.Second
)
var (
ErrServiceNotFound = errors.New("service not found")
ErrDiscoveryTimeout = errors.New("service discovery timeout for: %w")
ErrServiceDiscovery = errors.New("service discovery failed for: %w")
ErrNoValidService = errors.New("no valid service found for: %w")
)
func loadPredefinedAliases() map[string][]string {
return map[string][]string{
"clam": {"clamav-daemon.service", "clamd@scan.service", "clamd"},
"freshclam": {"clamav-freshclam.service", "freshclam.service"},
"fail2ban": {"fail2ban.service", "fail2ban"},
"supervisor": {"supervisord.service", "supervisor.service", "supervisord", "supervisor"},
"ssh": {"sshd.service", "ssh.service", "sshd", "ssh"},
"1panel": {"1panel.service", "1paneld"},
"docker": {"docker.service", "dockerd"},
}
}
func InitializeServiceDiscovery() {
svcName := loadAliasesFromConfig()
if len(svcName) > 0 {
RegisterServiceAliases(svcName)
}
}
func RegisterServiceAliases(aliases map[string][]string) {
for key, values := range aliases {
existing, loaded := serviceAliases.LoadOrStore(key, values)
if loaded {
merged := append(existing.([]string), values...)
serviceAliases.Store(key, merged)
}
}
}
func loadAliasesFromConfig() map[string][]string {
data, err := os.ReadFile(aliasFile)
if err != nil {
return nil
}
var rawAliases map[string][]string
json.Unmarshal(data, &rawAliases)
validAliases := make(map[string][]string)
for key, aliases := range rawAliases {
valid := []string{}
for _, alias := range aliases {
confirmed, _ := confirmServiceExists(alias)
if confirmed {
valid = append(valid, alias)
}
}
if len(valid) > 0 {
validAliases[key] = valid
}
}
return validAliases
}
func cleanupKeywordAliases(keyword string) {
serviceAliases.Range(func(k, v interface{}) bool {
if k.(string) != keyword {
return true
}
aliases := v.([]string)
valid := make([]string, 0)
for _, alias := range aliases {
confirmed, _ := confirmServiceExists(alias)
if confirmed {
valid = append(valid, alias)
}
}
if len(valid) == 0 {
serviceAliases.Delete(k)
serviceExistenceCache.Delete(k)
} else {
serviceAliases.Store(k, valid)
}
return true
})
go scheduleSave()
}
func smartServiceName(keyword string) (string, error) {
mgr := GetGlobalManager()
processedName := handleServiceNaming(mgr, keyword)
confirmed, _ := confirmServiceExists(processedName)
if confirmed {
updateAliases(keyword, processedName)
return processedName, nil
}
candidates := append([]string{processedName}, getAliases(keyword)...)
if name, err := validateCandidatesConcurrently(candidates); err == nil {
updateAliases(keyword, name)
return name, nil
}
discoveredName, err := discoverAndSelectService(keyword)
if err != nil {
cleanupKeywordAliases(keyword)
return "", ErrServiceNotFound
}
updateAliases(keyword, discoveredName)
return discoveredName, nil
}
func handleServiceNaming(mgr ServiceManager, keyword string) string {
keyword = strings.ToLower(keyword)
if strings.HasSuffix(keyword, ".service.socket") {
keyword = strings.TrimSuffix(keyword, ".service.socket") + ".socket"
}
if mgr.Name() != "systemd" {
keyword = strings.TrimSuffix(keyword, ".service")
return keyword
}
if !strings.HasSuffix(keyword, ".service") &&
!strings.HasSuffix(keyword, ".socket") {
keyword += ".service"
}
return keyword
}
func validateCandidatesConcurrently(candidates []string) (string, error) {
var (
g errgroup.Group
found = make(chan string, 1)
)
for _, candidate := range candidates {
cand := candidate
g.Go(func() error {
confirmed, _ := confirmServiceExists(cand)
if confirmed {
select {
case found <- cand:
default:
}
return nil
}
return ErrServiceNotFound
})
}
resultErr := make(chan error, 1)
go func() {
defer close(found)
resultErr <- g.Wait()
}()
select {
case name := <-found:
return name, nil
case <-time.After(1000 * time.Millisecond):
return "", fmt.Errorf(ErrDiscoveryTimeout.Error(), candidates[0])
case err := <-resultErr:
if err != nil {
return "", ErrServiceNotFound
}
return "", ErrServiceNotFound
}
}
func discoverAndSelectService(keyword string) (string, error) {
discovered, err := discoverServices(keyword)
if err != nil {
return "", ErrServiceNotFound
}
if len(discovered) == 0 {
return "", ErrServiceNotFound
}
selected, err := selectBestMatch(keyword, discovered)
if err != nil {
return "", ErrServiceNotFound
}
confirmed, err := confirmServiceExists(selected)
if err != nil {
return "", fmt.Errorf("service existence check failed: %w", err)
}
if confirmed {
return selected, nil
}
return "", ErrServiceNotFound
}
func selectBestMatch(keyword string, candidates []string) (string, error) {
if len(candidates) == 0 {
return "", ErrServiceNotFound
}
lowerKeyword := strings.ToLower(keyword)
var exactMatch string
var firstContainMatch string
for _, name := range candidates {
if strings.EqualFold(name, keyword) {
exactMatch = name
break
}
}
if exactMatch != "" {
return exactMatch, nil
}
for _, name := range candidates {
if strings.Contains(strings.ToLower(name), lowerKeyword) {
firstContainMatch = name
global.LOG.Debugf("[%s] [keyword: %s] Found first contain match: %s", getManagerName(), keyword, firstContainMatch)
break
}
}
if firstContainMatch != "" {
return firstContainMatch, nil
}
return "", fmt.Errorf("%w: %q (no exact or partial match)", ErrNoValidService, keyword)
}
type cacheItem struct {
services []string
expires time.Time
exists bool
}
var (
discoveryCache sync.Map
discoveryGroup singleflight.Group
)
func discoverServices(keyword string) ([]string, error) {
result, err := discoveryGroup.Do(keyword, func() (interface{}, error) {
if cached, ok := discoveryCache.Load(keyword); ok {
item := cached.(cacheItem)
if time.Now().Before(item.expires) {
return item.services, nil
}
discoveryCache.Delete(keyword)
}
manager := GetGlobalManager()
results, err := manager.FindServices(keyword)
if err != nil {
global.LOG.Errorf("Find services failed for %s: %v", keyword, err)
return nil, fmt.Errorf("%w: %q (%v)", ErrServiceDiscovery, keyword, err)
} else {
discoveryCache.Store(keyword, cacheItem{
services: results,
expires: time.Now().Add(5 * time.Minute),
})
}
return results, err
})
if err != nil {
return nil, err
}
return result.([]string), nil
}
func updateAliases(keyword, alias string) {
if keyword == alias {
return
}
existing, _ := serviceAliases.LoadOrStore(keyword, []string{})
aliases := existing.([]string)
if contains(aliases, alias) {
return
}
serviceAliases.Store(keyword, append(aliases, alias))
go scheduleSave()
}
func scheduleSave() {
saveMutex.Lock()
defer saveMutex.Unlock()
if saveTimer != nil {
saveTimer.Stop()
}
dataSnapshot := make(map[string][]string)
serviceAliases.Range(func(k, v interface{}) bool {
dataSnapshot[k.(string)] = append([]string{}, v.([]string)...)
return true
})
aliasFile = filepath.Join(constant.ResourceDir, "svcaliases.json")
saveTimer = time.AfterFunc(afterSaveTime, func() {
tmpFile := aliasFile + ".tmp"
if err := saveAliasesToFile(dataSnapshot, tmpFile); err == nil {
os.Rename(tmpFile, aliasFile)
}
})
}
func saveAliasesToFile(data map[string][]string, path string) error {
fileData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("serialization failed: %w", err)
}
if err := os.WriteFile(path, fileData, 0644); err != nil {
return fmt.Errorf("file write failed: %w", err)
}
return nil
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
var serviceExistenceCache sync.Map
func confirmServiceExists(serviceName string) (bool, error) {
if val, ok := serviceExistenceCache.Load(serviceName); ok {
if item, ok := val.(cacheItem); ok && time.Now().Before(item.expires) {
return item.exists, nil
}
serviceExistenceCache.Delete(serviceName)
}
handler := NewServiceHandler(defaultServiceConfig(serviceName))
isExist, err := handler.IsExists()
if err != nil {
return false, fmt.Errorf("check service existence failed: %w", err)
}
serviceExistenceCache.Store(serviceName, cacheItem{
exists: isExist.IsExists,
expires: time.Now().Add(30 * time.Second),
})
return isExist.IsExists, nil
}
func getAliases(keyword string) []string {
predefined := loadPredefinedAliases()[keyword]
runtimeAliases, _ := serviceAliases.LoadOrStore(keyword, []string{})
merged := make(map[string]struct{})
for _, alias := range predefined {
merged[alias] = struct{}{}
}
for _, alias := range runtimeAliases.([]string) {
merged[alias] = struct{}{}
}
result := make([]string, 0, len(merged))
for k := range merged {
result = append(result, k)
}
return result
}
type ConfigOption struct {
TailLines string
}
func ViewConfig(path string, opt ConfigOption) (string, error) {
var cmd []string
if opt.TailLines != "" && opt.TailLines != "0" {
cmd = []string{"tail", "-n", opt.TailLines, path}
} else {
cmd = []string{"cat", path}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
output, err := executeCommand(ctx, cmd[0], cmd[1:]...)
if err != nil {
return "", fmt.Errorf("view config failed: %w", err)
}
return string(output), nil
}
+283
View File
@@ -0,0 +1,283 @@
package systemctl
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
)
type ServiceConfig struct {
ServiceName map[string]string
}
type ServiceHandler struct {
config *ServiceConfig
manager ServiceManager
}
func NewServiceHandler(serviceNames map[string]string) *ServiceHandler {
mgr := GetGlobalManager()
if mgr == nil {
global.LOG.Error("failed to get global service manager when creating ServiceHandler")
return nil
}
return &ServiceHandler{
config: &ServiceConfig{
ServiceName: serviceNames,
},
manager: mgr,
}
}
type ServiceStatus struct {
IsActive bool `json:"isActive"`
IsEnabled bool `json:"isEnabled"`
IsExists bool `json:"isExists"`
Output string `json:"output"`
}
type ServiceIsActive struct {
IsActive bool `json:"isActive"`
Output string `json:"output"`
}
type ServiceIsEnabled struct {
IsEnabled bool `json:"isEnabled"`
Output string `json:"output"`
}
type ServiceResult struct {
Success bool `json:"success"`
Message string `json:"message"`
Output string `json:"output"`
}
var (
BinaryPath = "/usr/local/bin" // 1panl service default path
ErrServiceNotExist = errors.New("service does not exist")
)
func defaultServiceConfig(serviceName string) map[string]string {
mgr := getManagerName()
if mgr == "" {
global.LOG.Error("failed to get manager name for default service config")
return nil
}
return map[string]string{
mgr: serviceName,
}
}
func (h *ServiceHandler) ManagerName() string { return h.manager.Name() }
func getManagerName() string {
if mgr := GetGlobalManager(); mgr != nil {
return mgr.Name()
}
global.LOG.Error("failed to get global service manager")
return ""
}
func (h *ServiceHandler) GetServiceName() string {
manager := h.ManagerName()
if manager == "" {
global.LOG.Error("manager name is empty when getting service name")
return ""
}
return h.config.ServiceName[manager]
}
func (h *ServiceHandler) GetServicePath() (string, error) {
manager := h.ManagerName()
serviceName := h.config.ServiceName[manager]
if serviceName == "" {
err := fmt.Errorf("service name not found for %s", manager)
global.LOG.Errorf("GetServicePath error: %v", err)
return "", err
}
cleanPath := filepath.Clean(serviceName)
if strings.Contains(cleanPath, "..") {
err := fmt.Errorf("invalid path: %q", cleanPath)
global.LOG.Errorf("GetServicePath security check failed: %v", err)
return "", err
}
switch manager {
case "systemd":
return findSystemdPath(cleanPath)
case "openrc", "sysvinit":
return checkInitDPath(cleanPath)
default:
err := fmt.Errorf("unsupported init system: %s", manager)
global.LOG.Errorf("GetServicePath error: %v", err)
return "", err
}
}
func findSystemdPath(name string) (string, error) {
paths := []string{"/etc/systemd/system", "/usr/lib/systemd/system",
"/usr/share/systemd/system", "/usr/local/lib/systemd/system"}
for _, p := range paths {
if path := filepath.Join(p, name); FileExist(path) {
return path, nil
}
}
err := fmt.Errorf("service path not found for %s", name)
global.LOG.Errorf("findSystemdPath error: %v", err)
return "", err
}
func checkInitDPath(name string) (string, error) {
path := filepath.Join("/etc/init.d", name)
if !FileExist(path) {
err := fmt.Errorf("service path not found for %s", name)
global.LOG.Errorf("checkInitDPath error: %v", err)
return "", err
}
return path, nil
}
func (h *ServiceHandler) ExecuteAction(action string) (ServiceResult, error) {
successMsg := fmt.Sprintf("%s : %s completed", action, h.GetServiceName())
return h.executeAction(action, successMsg)
}
func (h *ServiceHandler) IsExists() (ServiceStatus, error) {
manager := GetGlobalManager()
isExist, _ := manager.ServiceExists(h.config)
return ServiceStatus{
IsExists: isExist,
}, nil
}
func (h *ServiceHandler) IsActive() (ServiceStatus, error) {
manager := GetGlobalManager()
if manager == nil {
global.LOG.Error("service manager not initialized during active check")
return ServiceStatus{}, fmt.Errorf("service manager not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
activeCmd, err := manager.BuildCommand("status", h.config)
if err != nil {
global.LOG.Errorf("Build status command failed: %v", err)
return ServiceStatus{}, fmt.Errorf("build status command failed: %w", err)
}
output, err := executeCommand(ctx, activeCmd[0], activeCmd[1:]...)
if err != nil {
if strings.Contains(err.Error(), "inactive") {
return ServiceStatus{
IsExists: false,
}, nil
}
global.LOG.Errorf("Active check execution failed: %v", err)
return ServiceStatus{}, fmt.Errorf("status check failed: %w", err)
}
isActive, err := manager.ParseStatus(string(output), h.config, "active")
if err != nil {
global.LOG.Warnf("Status parse error: %v", err)
}
return ServiceStatus{
IsActive: isActive,
Output: string(output),
}, nil
}
func (h *ServiceHandler) IsEnabled() (ServiceStatus, error) {
manager := GetGlobalManager()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
enabledCmd, err := manager.BuildCommand("is-enabled", h.config)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return ServiceStatus{
IsEnabled: false,
}, nil
}
global.LOG.Errorf("Build is-enabled command failed: %v", err)
return ServiceStatus{}, fmt.Errorf("build enabled check command failed: %w", err)
}
output, err := executeCommand(ctx, enabledCmd[0], enabledCmd[1:]...)
if err != nil {
if strings.Contains(err.Error(), "disabled") {
return ServiceStatus{
IsEnabled: false,
}, nil
}
// // isEnabled, err := h.ParseStatus(string(output), h.config, "enabled")
// global.LOG.Errorf("Enabled check execution failed: %v", err)
// return ServiceStatus{}, fmt.Errorf("enabled check failed: %w", err)
}
isEnabled, err := manager.ParseStatus(string(output), h.config, "enabled")
if err != nil {
global.LOG.Warnf("Enabled status parse error: %v", err)
}
return ServiceStatus{
IsEnabled: isEnabled,
Output: string(output),
}, nil
}
func (h *ServiceHandler) StartService() (ServiceResult, error) {
return h.ExecuteAction("start")
}
func (h *ServiceHandler) StopService() (ServiceResult, error) {
return h.ExecuteAction("stop")
}
func (h *ServiceHandler) RestartService() (ServiceResult, error) {
return h.ExecuteAction("restart")
}
func (h *ServiceHandler) EnableService() (ServiceResult, error) {
return h.ExecuteAction("enable")
}
func (h *ServiceHandler) DisableService() (ServiceResult, error) {
return h.ExecuteAction("disable")
}
func (h *ServiceHandler) executeAction(action, successMsg string) (ServiceResult, error) {
manager := GetGlobalManager()
if manager == nil {
global.LOG.Error("service manager not initialized during action execution")
return ServiceResult{}, fmt.Errorf("service manager not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), defaultCommandTimeout)
defer cancel()
cmdArgs, err := manager.BuildCommand(action, h.config)
if err != nil {
global.LOG.Errorf("Build command failed for action %s: %v", action, err)
return ServiceResult{}, fmt.Errorf("build command failed: %w", err)
}
output, err := executeCommand(ctx, cmdArgs[0], cmdArgs[1:]...)
if err != nil {
global.LOG.Errorf("%s operation failed: %v", action, err)
return ServiceResult{
Success: false,
Message: fmt.Sprintf("%s failed", action),
Output: string(output),
}, fmt.Errorf("%s operation failed: %w", action, err)
}
global.LOG.Infof("[%s]: %s", manager.Name(), successMsg)
return ServiceResult{
Success: true,
Message: successMsg,
Output: string(output),
}, nil
}
+440
View File
@@ -0,0 +1,440 @@
package systemctl
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
)
var (
managers = make(map[string]ServiceManager)
mu sync.RWMutex
globalManager ServiceManager
managerPriority = []string{"systemd", "openrc", "sysvinit"}
)
const (
defaultCommandTimeout = 60 * time.Second
serviceCheckTimeout = 5 * time.Second
)
type ServiceManager interface {
Name() string
IsAvailable() bool
ServiceExists(*ServiceConfig) (bool, error)
BuildCommand(string, *ServiceConfig) ([]string, error)
ParseStatus(string, *ServiceConfig, string) (bool, error)
FindServices(string) ([]string, error)
}
type baseManager struct {
name string
cmdTool string
activeRegex *regexp.Regexp
enabledRegex *regexp.Regexp
}
func (b *baseManager) Name() string { return b.name }
func isRootUser() bool {
return os.Geteuid() == 0
}
func (b *baseManager) buildBaseCommand() []string {
var cmdArgs []string
if !isRootUser() {
cmdArgs = append(cmdArgs, "sudo")
}
cmdArgs = append(cmdArgs, b.cmdTool)
return cmdArgs
}
func (b *baseManager) commonServiceExists(config *ServiceConfig, checkFn func(string) (bool, error)) (bool, error) {
if name := config.ServiceName[b.name]; name != "" {
exists, checkErr := checkFn(name)
if checkErr != nil {
return false, nil
}
return exists, nil
}
return false, nil
}
func (b *baseManager) ParseStatus(output string, _ *ServiceConfig, statusType string) (bool, error) {
if output == "" {
return false, nil
}
switch statusType {
case "active":
if b.activeRegex == nil {
return false, nil
}
return b.activeRegex.MatchString(output), nil
case "enabled":
if b.enabledRegex == nil {
return false, nil
}
return b.enabledRegex.MatchString(output), nil
default:
return false, nil
}
}
func registerManager(m ServiceManager) {
mu.Lock()
defer mu.Unlock()
managers[m.Name()] = m
}
func init() {
for _, mgr := range []ServiceManager{
newSystemdManager(),
newOpenrcManager(),
newSysvinitManager(),
} {
registerManager(mgr)
}
}
func InitializeGlobalManager() (err error) {
for _, name := range managerPriority {
if mgr, ok := managers[name]; ok && mgr.IsAvailable() {
if testManager(mgr) {
globalManager = mgr
global.LOG.Infof("Initialized service manager: %s", name)
return
}
}
}
err = fmt.Errorf("no available service manager found (tried: %v)", managerPriority)
return
}
func testManager(mgr ServiceManager) bool {
_, err := mgr.BuildCommand("status", &ServiceConfig{
ServiceName: map[string]string{mgr.Name(): "test-service"},
})
return err == nil
}
func GetGlobalManager() ServiceManager {
if globalManager == nil {
mu.Lock()
defer mu.Unlock()
if globalManager == nil {
return initializeWithRetry()
}
}
return globalManager
}
func initializeWithRetry() ServiceManager {
const (
maxRetries = 5
initialWait = 1 * time.Second
)
backoff := initialWait
for attempt := 1; attempt <= maxRetries; attempt++ {
if err := InitializeGlobalManager(); err == nil {
return globalManager
}
logMessage := fmt.Sprintf("Manager init attempt %d/%d failed", attempt, maxRetries)
if global.LOG != nil {
global.LOG.Warn(logMessage)
} else {
fmt.Printf("[WARN] %s\n", logMessage)
}
if attempt < maxRetries {
time.Sleep(backoff)
backoff *= 2
}
}
if global.LOG != nil {
global.LOG.Error("All manager initialization attempts failed")
} else {
fmt.Println("[FATAL] All manager initialization attempts failed")
}
panic("unable to initialize service manager")
}
func executeCommand(ctx context.Context, command string, args ...string) ([]byte, error) {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, defaultCommandTimeout)
defer cancel()
}
cmd := exec.CommandContext(ctx, command, args...)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Run(); err != nil {
return buf.Bytes(), &CommandError{
Cmd: cmd.String(),
Output: buf.String(),
Err: err,
}
}
return buf.Bytes(), nil
}
type systemdManager struct{ baseManager }
func newSystemdManager() ServiceManager {
return &systemdManager{baseManager{
name: "systemd",
cmdTool: "systemctl",
activeRegex: regexp.MustCompile(`(?i)Active:\s+active\b`),
enabledRegex: regexp.MustCompile(`(?i)^\s*enabled\s*$`),
}}
}
func (m *systemdManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *systemdManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), serviceCheckTimeout)
defer cancel()
out, cmdErr := executeCommand(ctx, m.cmdTool, "is-enabled", name)
if cmdErr == nil || strings.Contains(string(out), "disabled") {
return true, nil
}
return false, nil
})
}
func (m *systemdManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
cmdArgs := m.buildBaseCommand()
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
cmdArgs = append(cmdArgs, "is-enabled", service)
default:
cmdArgs = append(cmdArgs, action, service)
}
return cmdArgs, nil
}
func (m *systemdManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
if strings.Contains(output, "could not be found") {
return false, nil
}
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
return false, nil
}
return result, nil
}
func (m *systemdManager) FindServices(keyword string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
out, err := executeCommand(ctx, m.cmdTool, "list-unit-files", "--type=service", "--no-legend")
if err != nil {
return nil, fmt.Errorf("failed to list systemd services: %w", err)
}
var services []string
lines := strings.Split(string(out), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 1 {
continue
}
serviceName := fields[0]
if strings.Contains(serviceName, keyword) {
services = append(services, serviceName)
}
}
return services, nil
}
type sysvinitManager struct{ baseManager }
func newSysvinitManager() ServiceManager {
return &sysvinitManager{baseManager{
name: "sysvinit",
cmdTool: "service",
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
}}
}
func (m *sysvinitManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *sysvinitManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
_, err := os.Stat(filepath.Join("/etc/init.d", name))
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat /etc/init.d/%s failed: %w", name, err)
}
return true, nil
})
}
func (m *sysvinitManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
return []string{
"sh",
"-c",
fmt.Sprintf("if ls /etc/rc*.d/S*%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
case "is-active", "status":
return []string{
"sh",
"-c",
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
}, nil
default:
cmdArgs := m.buildBaseCommand()
cmdArgs = append(cmdArgs, service, action)
return cmdArgs, nil
}
}
func (m *sysvinitManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
serviceName := config.ServiceName[m.name]
switch statusType {
case "enabled":
if strings.Contains(output, "no such file or directory") {
return false, nil
}
return strings.TrimSpace(output) != "", nil
case "active":
if strings.Contains(output, "not found") {
return false, nil
}
if strings.Contains(output, "running") || strings.Contains(output, "active") {
return true, nil
}
default:
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
global.LOG.Debugf("[sysvinit] Status parse failed. [ServiceName:%s] Type: %s, Output: %q", serviceName, statusType, output)
}
return result, err
}
return false, fmt.Errorf("unsupported status type: %s", statusType)
}
func (m *sysvinitManager) FindServices(keyword string) ([]string, error) {
files, err := os.ReadDir("/etc/init.d/")
if err != nil {
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
}
var services []string
for _, file := range files {
if strings.Contains(file.Name(), keyword) {
services = append(services, file.Name())
}
}
return services, nil
}
type openrcManager struct{ baseManager }
func newOpenrcManager() ServiceManager {
return &openrcManager{baseManager{
name: "openrc",
cmdTool: "rc-service",
// activeRegex: regexp.MustCompile(`(?i)^\s*status:\s+(started|running|active)\s*$`),
// enabledRegex: regexp.MustCompile(`(?i)^[^\|]+\|\s*(default|enabled)\b.*$`),
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
}}
}
func (m *openrcManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *openrcManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
_, err := os.Stat(filepath.Join("/etc/init.d", name))
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat /etc/init.d/%s failed: %w", name, err)
}
return true, nil
})
}
func (m *openrcManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
cmdArgs := m.buildBaseCommand()
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
return []string{
"sh",
"-c",
fmt.Sprintf("if ls /etc/runlevels/default/%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
case "is-active", "status":
return []string{
"sh",
"-c",
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
}, nil
case "enable":
return []string{"rc-update", "add", service, "default"}, nil
case "disable":
return []string{"rc-update", "del", service, "default"}, nil
default:
cmdArgs = append(cmdArgs, service, action)
return cmdArgs, nil
}
}
func (m *openrcManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
if strings.Contains(output, "does not exist") {
return false, nil
}
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
return false, nil
}
return result, nil
}
func (m *openrcManager) FindServices(keyword string) ([]string, error) {
files, err := os.ReadDir("/etc/init.d/")
if err != nil {
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
}
keyword = strings.ToLower(keyword)
var services []string
for _, file := range files {
if strings.Contains(file.Name(), keyword) {
services = append(services, file.Name())
}
}
return services, nil
}
type CommandError struct {
Cmd string
Err error
Output string
}
func (e CommandError) Error() string {
return fmt.Sprintf("command %q failed: %v \nOutput: %s",
e.Cmd, e.Err, e.Output)
}
+121 -31
View File
@@ -2,63 +2,153 @@ package systemctl
import ( import (
"fmt" "fmt"
"github.com/pkg/errors" "os"
"os/exec"
"strings" "github.com/1Panel-dev/1Panel/backend/global"
) )
func RunSystemCtl(args ...string) (string, error) { func DefaultHandler(serviceName string) (*ServiceHandler, error) {
cmd := exec.Command("systemctl", args...) svcName, err := smartServiceName(serviceName)
output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return string(output), fmt.Errorf("failed to run command: %w", err) return nil, ErrServiceNotFound
} }
return string(output), nil return NewServiceHandler(defaultServiceConfig(svcName)), nil
} }
func IsActive(serviceName string) (bool, error) { func GetServiceName(serviceName string) (string, error) {
out, err := RunSystemCtl("is-active", serviceName) serviceName, err := smartServiceName(serviceName)
if err != nil { if err != nil {
return false, err return "", ErrServiceNotFound
} }
return out == "active\n", nil return serviceName, nil
} }
func IsEnable(serviceName string) (bool, error) { func GetServicePath(serviceName string) (string, error) {
out, err := RunSystemCtl("is-enabled", serviceName) handler, err := DefaultHandler(serviceName)
if err != nil { if err != nil {
return false, err return "", ErrServiceNotFound
} }
return out == "enabled\n", nil return handler.GetServicePath()
}
func CustomAction(action string, serviceName string) (ServiceResult, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
global.LOG.Errorf("CustomAction handler init failed: %v", err)
return ServiceResult{}, ErrServiceNotFound
}
result, err := handler.ExecuteAction(action)
if err != nil {
global.LOG.Errorf("CustomAction %s failed: %v", action, err)
return result, fmt.Errorf("%s operation failed: %w | Output: %s", action, err, result.Output)
}
return result, nil
} }
func IsExist(serviceName string) (bool, error) { func IsExist(serviceName string) (bool, error) {
out, err := RunSystemCtl("is-enabled", serviceName) handler, err := DefaultHandler(serviceName)
if err != nil { if err != nil {
if strings.Contains(out, "disabled") {
return true, nil
}
return false, nil return false, nil
} }
return true, nil result, _ := handler.IsExists()
if result.IsExists {
return true, nil
} else {
return false, nil
}
} }
func handlerErr(out string, err error) error { func Start(serviceName string) error {
handler, _ := DefaultHandler(serviceName)
result, err := handler.StartService()
if err != nil { if err != nil {
if out != "" { global.LOG.Errorf("Service start failed: %v | Output: %s", err, result.Output)
return errors.New(out) return fmt.Errorf("start failed: %v | Output: %s", err, result.Output)
} }
return err return nil
}
func Stop(serviceName string) error {
handler, err := DefaultHandler(serviceName)
if err != nil {
global.LOG.Errorf("Stop handler init failed: %v", err)
return fmt.Errorf("%s is not exist", serviceName)
}
result, err := handler.StopService()
if err != nil {
global.LOG.Errorf("Service stop failed: %v", err)
return fmt.Errorf("stop failed: %v | Output: %s", err, result.Output)
} }
return nil return nil
} }
func Restart(serviceName string) error { func Restart(serviceName string) error {
out, err := RunSystemCtl("restart", serviceName) handler, err := DefaultHandler(serviceName)
return handlerErr(out, err) if err != nil {
global.LOG.Errorf("Restart handler init failed: %v", err)
return fmt.Errorf("%s is not exist", serviceName)
}
result, err := handler.RestartService()
if err != nil {
global.LOG.Errorf("Service restart failed: %v", err)
return fmt.Errorf("restart failed: %v | Output: %s", err, result.Output)
}
return nil
} }
func Operate(operate, serviceName string) error { func Enable(serviceName string) error {
out, err := RunSystemCtl(operate, serviceName) handler, err := DefaultHandler(serviceName)
return handlerErr(out, err) if err != nil {
global.LOG.Errorf("Enable handler init failed: %v", err)
return fmt.Errorf("%s is not exist", serviceName)
}
result, err := handler.EnableService()
if err != nil {
global.LOG.Errorf("Service enable failed: %v | Output: %s", err, result.Output)
return fmt.Errorf("%s enable failed: %v ", serviceName, err)
}
return nil
}
func Disable(serviceName string) error {
handler, _ := DefaultHandler(serviceName)
result, err := handler.DisableService()
if err != nil {
global.LOG.Errorf("Service disable failed: %v", err)
return fmt.Errorf("disable failed: %v | Output: %s", err, result.Output)
}
return nil
}
func IsActive(serviceName string) (bool, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
return false, nil
}
status, err := handler.IsActive()
if err != nil {
return false, nil
}
return status.IsActive, nil
}
func IsEnable(serviceName string) (bool, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
return false, nil
}
status, err := handler.IsEnabled()
if err != nil {
return false, nil
}
return status.IsEnabled, nil
}
type LogOption struct {
TailLines string
}
func FileExist(path string) bool {
_, err := os.Stat(path)
return err == nil
} }
+3 -7
View File
@@ -25,8 +25,8 @@ type LocalCommand struct {
pty *os.File pty *os.File
} }
func NewCommand(initCmd string) (*LocalCommand, error) { func NewCommand(initCmd []string) (*LocalCommand, error) {
cmd := exec.Command("bash") cmd := exec.Command("docker", initCmd...)
if term := os.Getenv("TERM"); term != "" { if term := os.Getenv("TERM"); term != "" {
cmd.Env = append(os.Environ(), "TERM="+term) cmd.Env = append(os.Environ(), "TERM="+term)
} else { } else {
@@ -38,11 +38,6 @@ func NewCommand(initCmd string) (*LocalCommand, error) {
return nil, errors.Wrapf(err, "failed to start command") return nil, errors.Wrapf(err, "failed to start command")
} }
if len(initCmd) != 0 {
time.Sleep(100 * time.Millisecond)
_, _ = pty.Write([]byte(initCmd + "\n"))
}
lcmd := &LocalCommand{ lcmd := &LocalCommand{
closeSignal: DefaultCloseSignal, closeSignal: DefaultCloseSignal,
closeTimeout: DefaultCloseTimeout, closeTimeout: DefaultCloseTimeout,
@@ -100,4 +95,5 @@ func (lcmd *LocalCommand) Wait(quitChan chan bool) {
global.LOG.Errorf("ssh session wait failed, err: %v", err) global.LOG.Errorf("ssh session wait failed, err: %v", err)
setQuit(quitChan) setQuit(quitChan)
} }
setQuit(quitChan)
} }
+115 -27
View File
@@ -3,6 +3,7 @@ package toolbox
import ( import (
"fmt" "fmt"
"os" "os"
"regexp"
"strings" "strings"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
@@ -22,15 +23,15 @@ type FirewallClient interface {
} }
func NewFail2Ban() (*Fail2ban, error) { func NewFail2Ban() (*Fail2ban, error) {
isExist, _ := systemctl.IsExist("fail2ban.service") isExist, _ := systemctl.IsExist("fail2ban")
if isExist { if isExist {
if _, err := os.Stat(defaultPath); err != nil { if _, err := os.Stat(defaultPath); err != nil {
if err := initLocalFile(); err != nil { if err := initLocalFile(); err != nil {
return nil, err return nil, err
} }
stdout, err := cmd.Exec("systemctl restart fail2ban.service") err := systemctl.Restart("fail2ban")
if err != nil { if err != nil {
global.LOG.Errorf("restart fail2ban failed, err: %s", stdout) global.LOG.Errorf("restart fail2ban failed, err: %s", err)
return nil, err return nil, err
} }
} }
@@ -47,20 +48,25 @@ func (f *Fail2ban) Status() (bool, bool, bool) {
} }
func (f *Fail2ban) Version() string { func (f *Fail2ban) Version() string {
stdout, err := cmd.Exec("fail2ban-client version") stdout, err := cmd.Exec("fail2ban-client --version")
if err != nil { if err != nil {
global.LOG.Errorf("load the fail2ban version failed, err: %s", stdout) global.LOG.Errorf("load the fail2ban version failed, err: %s", stdout)
return "-" return "-"
} }
return strings.ReplaceAll(stdout, "\n", "") versionRe := regexp.MustCompile(`(?i)fail2ban[:\s-]*v?(\d+\.\d+\.\d+)`)
matches := versionRe.FindStringSubmatch(stdout)
if len(matches) > 1 {
return matches[1]
}
global.LOG.Errorf("Version regex failed to match output: %s", stdout)
return "-"
} }
func (f *Fail2ban) Operate(operate string) error { func (f *Fail2ban) Operate(operate string) error {
switch operate { switch operate {
case "start", "restart", "stop", "enable", "disable": case "start", "restart", "stop", "enable", "disable":
stdout, err := cmd.Execf("systemctl %s fail2ban.service", operate) stdout, err := systemctl.CustomAction(operate, "fail2ban")
if err != nil { if err != nil {
return fmt.Errorf("%s the fail2ban.service failed, err: %s", operate, stdout) return fmt.Errorf("%s the fail2ban failed, err: %s", operate, stdout.Output)
} }
return nil return nil
case "reload": case "reload":
@@ -136,7 +142,32 @@ func initLocalFile() error {
return err return err
} }
defer f.Close() defer f.Close()
initFile := `#DEFAULT-START var initFile string
if systemctl.GetGlobalManager().Name() == "openrc" {
initFile = `[sshd]
enabled = true
filter = alpine-sshd
port = $ssh_port
logpath = $logpath
maxretry = 2
banaction = $banaction
[sshd-ddos]
enabled = true
filter = alpine-sshd-ddos
port = $ssh_port
logpath = /var/log/messages
maxretry = 2
[sshd-key]
enabled = true
filter = alpine-sshd-key
port = $ssh_port
logpath = /var/log/messages
maxretry = 2
`
} else {
initFile = `# DEFAULT-START
[DEFAULT] [DEFAULT]
bantime = 600 bantime = 600
findtime = 300 findtime = 300
@@ -146,36 +177,93 @@ action = %(action_mwl)s
#DEFAULT-END #DEFAULT-END
[sshd] [sshd]
ignoreip = 127.0.0.1/8 ignoreip = 127.0.0.1/8 ::1
enabled = true enabled = true
filter = sshd filter = sshd
port = 22 port = $ssh_port
maxretry = 5 maxretry = 5
findtime = 300 findtime = 300
bantime = 600 bantime = 600
banaction = $banaction banaction = $banaction
action = %(action_mwl)s action = %(action_mwl)s
logpath = $logpath` logpath = $logpath
maxmatches = 3
banaction := "" `
if active, _ := systemctl.IsActive("firewalld"); active {
banaction = "firewallcmd-ipset"
} else if active, _ := systemctl.IsActive("ufw"); active {
banaction = "ufw"
} else {
banaction = "iptables-allports"
} }
// 检测防火墙类型
banaction := detectFirewall()
// 检测SSH端口(支持Alpine的sshd_config位置)
sshPort := detectSSHPort()
// 检测日志路径(兼容Alpine的多种日志位置)
logPath := detectAuthLogPath()
// 执行变量替换
initFile = strings.ReplaceAll(initFile, "$banaction", banaction) initFile = strings.ReplaceAll(initFile, "$banaction", banaction)
logPath := ""
if _, err := os.Stat("/var/log/secure"); err == nil {
logPath = "/var/log/secure"
} else {
logPath = "/var/log/auth.log"
}
initFile = strings.ReplaceAll(initFile, "$logpath", logPath) initFile = strings.ReplaceAll(initFile, "$logpath", logPath)
initFile = strings.ReplaceAll(initFile, "$ssh_port", sshPort)
if err := os.WriteFile(defaultPath, []byte(initFile), 0640); err != nil { if err := os.WriteFile(defaultPath, []byte(initFile), 0640); err != nil {
return err return err
} }
return nil return nil
} }
func detectFirewall() string {
if active, _ := systemctl.IsActive("firewalld"); active {
return "firewallcmd-ipset"
}
if active, _ := systemctl.IsActive("ufw"); active {
return "ufw"
}
if active, _ := systemctl.IsActive("iptables"); active {
return "iptables-allports"
}
if active, _ := systemctl.IsActive("nftables"); active {
return "nftables-allports"
}
return "iptables-allports"
}
func detectAuthLogPath() string {
paths := []string{
"/var/log/auth.log", // 常见默认路径
"/var/log/messages", // Alpine系统日志
"/var/log/secure", // RHEL风格路径
"/var/log/syslog", // Debian风格路径
}
for _, path := range paths {
if _, err := os.Stat(path); err == nil {
return path
}
}
return "/var/log/auth.log"
}
func detectSSHPort() string {
// 检查标准sshd_config路径
configPaths := []string{
"/etc/ssh/sshd_config",
"/etc/sshd_config",
}
for _, path := range configPaths {
data, err := os.ReadFile(path)
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "Port") {
parts := strings.Fields(line)
if len(parts) >= 2 {
return parts[1]
}
}
}
}
return "22" // 默认SSH端口
}
+4 -3
View File
@@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"golang.org/x/crypto/bcrypt"
"os" "os"
"os/user" "os/user"
"path" "path"
@@ -12,6 +11,8 @@ import (
"strings" "strings"
"time" "time"
"golang.org/x/crypto/bcrypt"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
@@ -94,9 +95,9 @@ func (f *Ftp) Status() (bool, bool) {
func (f *Ftp) Operate(operate string) error { func (f *Ftp) Operate(operate string) error {
switch operate { switch operate {
case "start", "restart", "stop": case "start", "restart", "stop":
stdout, err := cmd.Execf("systemctl %s pure-ftpd.service", operate) stdout, err := systemctl.CustomAction(operate, "pure-ftpd")
if err != nil { if err != nil {
return fmt.Errorf("%s the pure-ftpd.service failed, err: %s", operate, stdout) return fmt.Errorf("%s the pure-ftpd service failed, err: %s", operate, stdout.Output)
} }
return nil return nil
default: default:
+20 -13
View File
@@ -6,25 +6,32 @@ command -v wget >/dev/null || {
} }
if [ ! -f "1pctl" ]; then if [ ! -f "1pctl" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/1pctl wget https://github.com/1Panel-dev/installer/raw/v1/1pctl
fi
if [ ! -f "1panel.service" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/1panel.service
fi fi
if [ ! -f "install.sh" ]; then if [ ! -f "install.sh" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/install.sh wget https://github.com/1Panel-dev/installer/raw/v1/install.sh
fi
if [ ! -d "initscript" ]; then
wget https://github.com/1Panel-dev/installer/raw/v1/initscript/1panel.service
mkdir -p initscript && cd initscript
for file in 1panel.service 1paneld.init 1paneld.openrc 1paneld.procd; do
wget -q https://github.com/1Panel-dev/installer/raw/v1/initscript/$file
done
cd ..
fi
if [ ! -d "lang" ]; then
mkdir -p lang && cd lang
for lang in en fa pt-BR ru zh; do
wget -q https://github.com/1Panel-dev/installer/raw/v1/lang/$lang.sh
done
cd ..
fi fi
if [ ! -f "GeoIP.mmdb" ]; then if [ ! -f "GeoIP.mmdb" ]; then
wget https://resource.1panel.pro/geo/GeoIP.mmdb wget https://resource.fit2cloud.com/1panel/package/geo/GeoIP.mmdb
fi
if [ ! -f "lang.tar.gz" ]; then
wget https://resource.1panel.pro/language/lang.tar.gz
tar zxvf lang.tar.gz
rm -rf lang.tar.gz
fi fi
chmod 755 1pctl install.sh chmod 755 1pctl install.sh
+16 -2
View File
@@ -58,8 +58,15 @@ var restoreCmd = &cobra.Command{
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo") geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
_, _ = cmdUtils.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpPath, "GeoIP.mmdb"), geoPath) _, _ = cmdUtils.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpPath, "GeoIP.mmdb"), geoPath)
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep3")) fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep3"))
if err := common.CopyFile(path.Join(tmpPath, "1panel.service"), "/etc/systemd/system"); err != nil { if isSystemd() {
return err if err := common.CopyFile(path.Join(tmpPath, "1panel.service"), "/etc/systemd/system"); err != nil {
return err
}
} else {
if err := common.CopyFile(path.Join(tmpPath, "1paneld"), "/etc/init.d"); err != nil {
return err
}
} }
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4")) fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4"))
checkPointOfWal() checkPointOfWal()
@@ -124,3 +131,10 @@ func handleUnTar(sourceFile, targetDir string) error {
} }
return nil return nil
} }
func isSystemd() bool {
if _, err := os.Stat("/etc/systemd/system"); err == nil {
return true
}
return false
}
+629 -626
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+378 -384
View File
@@ -3342,6 +3342,8 @@ definitions:
type: string type: string
lastCleanTime: lastCleanTime:
type: string type: string
licenseVerify:
type: string
localTime: localTime:
type: string type: string
menuTabs: menuTabs:
@@ -11237,381 +11239,6 @@ paths:
formatEN: update group [name][type] formatEN: update group [name][type]
formatZH: 更新组 [name][type] formatZH: 更新组 [name][type]
paramKeys: [] paramKeys: []
/host/conffile/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHConf'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting by file
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: update SSH conf
formatZH: 修改 SSH 配置文件
paramKeys: []
/host/ssh/conf:
get:
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH conf
tags:
- SSH
/host/ssh/generate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateSSH'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Generate host SSH secret
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: generate SSH secret
formatZH: '生成 SSH 密钥 '
paramKeys: []
/host/ssh/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchSSHLog'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHLog'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH logs
tags:
- SSH
/host/ssh/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate SSH
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] SSH'
formatZH: '[operation] SSH '
paramKeys: []
/host/ssh/search:
post:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHInfo'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH setting info
tags:
- SSH
/host/ssh/secret:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateLoad'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH secret
tags:
- SSH
/host/ssh/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- key
- value
formatEN: update SSH setting [key] => [value]
formatZH: 修改 SSH 配置 [key] => [value]
paramKeys: []
/host/tool:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolRes'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool status
tags:
- Host tool
/host/tool/config:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolConfig'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] tool config'
formatZH: '[operate] 主机工具配置文件 '
paramKeys: []
/host/tool/create:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Host tool Config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- type
formatEN: create [type] config
formatZH: 创建 [type] 配置
paramKeys: []
/host/tool/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolLogReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool logs
tags:
- Host tool
/host/tool/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate tool
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
- type
formatEN: '[operate] [type]'
formatZH: '[operate] [type] '
paramKeys: []
/host/tool/supervisor/process:
get:
consumes:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.SupervisorProcessConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessConfig'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Supervisor process
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] process'
formatZH: '[operate] 守护进程 '
paramKeys: []
/host/tool/supervisor/process/file:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessFileReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] Supervisor Process Config file'
formatZH: '[operate] Supervisor 进程文件 '
paramKeys: []
/hosts: /hosts:
post: post:
consumes: consumes:
@@ -11878,6 +11505,32 @@ paths:
formatEN: update quick command [name] formatEN: update quick command [name]
formatZH: 更新快捷命令 [name] formatZH: 更新快捷命令 [name]
paramKeys: [] paramKeys: []
/hosts/conffile/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHConf'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting by file
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: update SSH conf
formatZH: 修改 SSH 配置文件
paramKeys: []
/hosts/del: /hosts/del:
post: post:
consumes: consumes:
@@ -12197,6 +11850,157 @@ paths:
summary: Page host summary: Page host
tags: tags:
- Host - Host
/hosts/ssh/conf:
get:
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH conf
tags:
- SSH
/hosts/ssh/generate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateSSH'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Generate host SSH secret
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: generate SSH secret
formatZH: '生成 SSH 密钥 '
paramKeys: []
/hosts/ssh/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchSSHLog'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHLog'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH logs
tags:
- SSH
/hosts/ssh/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate SSH
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] SSH'
formatZH: '[operation] SSH '
paramKeys: []
/hosts/ssh/search:
post:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHInfo'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH setting info
tags:
- SSH
/hosts/ssh/secret:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateLoad'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH secret
tags:
- SSH
/hosts/ssh/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- key
- value
formatEN: update SSH setting [key] => [value]
formatZH: 修改 SSH 配置 [key] => [value]
paramKeys: []
/hosts/test/byid/{id}: /hosts/test/byid/{id}:
post: post:
consumes: consumes:
@@ -12240,6 +12044,204 @@ paths:
summary: Test host conn by info summary: Test host conn by info
tags: tags:
- Host - Host
/hosts/tool:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolRes'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool status
tags:
- Host tool
/hosts/tool/config:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolConfig'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] tool config'
formatZH: '[operate] 主机工具配置文件 '
paramKeys: []
/hosts/tool/create:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Host tool Config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- type
formatEN: create [type] config
formatZH: 创建 [type] 配置
paramKeys: []
/hosts/tool/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolLogReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool logs
tags:
- Host tool
/hosts/tool/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate tool
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
- type
formatEN: '[operate] [type]'
formatZH: '[operate] [type] '
paramKeys: []
/hosts/tool/supervisor/process:
get:
consumes:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.SupervisorProcessConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessConfig'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Supervisor process
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] process'
formatZH: '[operate] 守护进程 '
paramKeys: []
/hosts/tool/supervisor/process/file:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessFileReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] Supervisor Process Config file'
formatZH: '[operate] Supervisor 进程文件 '
paramKeys: []
/hosts/tree: /hosts/tree:
post: post:
consumes: consumes:
@@ -16766,15 +16768,7 @@ schemes:
- https - https
securityDefinitions: securityDefinitions:
ApiKeyAuth: ApiKeyAuth:
description: |- description: '- `1Panel-Token` is the key for the panel API Key.'
Custom Token Format, Format: md5('1panel' + API-Key + UnixTimestamp).
```
eg:
curl -X GET "http://localhost:4004/api/v1/dashboard/current" \
-H "1Panel-Token: <1panel_token>" \
-H "1Panel-Timestamp: <current_unix_timestamp>"
```
- `1Panel-Token` is the key for the panel API Key.
in: header in: header
name: 1Panel-Token name: 1Panel-Token
type: apiKey type: apiKey
+19
View File
@@ -0,0 +1,19 @@
services:
mcp-server:
image: supercorp/supergateway:latest
container_name: ${CONTAINER_NAME}
restart: unless-stopped
ports:
- "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${PANEL_APP_PORT_HTTP}"
command: [
"--stdio", "${COMMAND}",
"--port", "${PANEL_APP_PORT_HTTP}",
"--baseUrl", "${BASE_URL}",
"--ssePath", "${SSE_PATH}",
"--messagePath", "${SSE_PATH}/messages"
]
networks:
- 1panel-network
networks:
1panel-network:
external: true
+8
View File
@@ -0,0 +1,8 @@
package mcp
import (
_ "embed"
)
//go:embed compose.yml
var DefaultMcpCompose []byte
+3
View File
@@ -37,3 +37,6 @@ var DomainNotFoundHTML []byte
//go:embed stop.html //go:embed stop.html
var StopHTML []byte var StopHTML []byte
//go:embed sse.conf
var SSE []byte
+1
View File
@@ -7,6 +7,7 @@ location ^~ /test {
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection; proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1; proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status; add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache; add_header Cache-Control no-cache;
+7
View File
@@ -0,0 +1,7 @@
location ^~ /github {
proxy_pass http://127.0.0.1:8001/github;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection '';
chunked_transfer_encoding off;
}
+1
View File
@@ -28,3 +28,4 @@ stats.html
*.sln *.sln
*.sw? *.sw?
.vscode .vscode
*.code-workspace
+5 -4
View File
@@ -38,8 +38,9 @@
"axios": "^1.7.2", "axios": "^1.7.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dompurify": "^3.3.1",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"element-plus": "^2.7.5", "element-plus": "2.9.9",
"fit2cloud-ui-plus": "^1.2.0", "fit2cloud-ui-plus": "^1.2.0",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
@@ -64,8 +65,8 @@
"@types/node": "^20.15.0", "@types/node": "^20.15.0",
"@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/parser": "^5.22.0",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.0.0", "@vitejs/plugin-vue-jsx": "^5.1.1",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"commitizen": "^4.2.4", "commitizen": "^4.2.4",
"eslint": "^8.43.0", "eslint": "^8.43.0",
@@ -84,7 +85,7 @@
"typescript": "^4.5.4", "typescript": "^4.5.4",
"unplugin-auto-import": "^0.16.4", "unplugin-auto-import": "^0.16.4",
"unplugin-vue-components": "^0.25.0", "unplugin-vue-components": "^0.25.0",
"vite": "^6.0.7", "vite": "^7.1.5",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2",
+71
View File
@@ -109,4 +109,75 @@ export namespace AI {
websiteID?: number; websiteID?: number;
connUrl: string; connUrl: string;
} }
export interface Environment {
key: string;
value: string;
}
export interface Volume {
source: string;
target: string;
}
export interface McpServer {
id: number;
name: string;
status: string;
baseUrl: string;
ssePath: string;
command: string;
port: number;
message: string;
createdAt?: string;
containerName: string;
environments: Environment[];
volumes: Volume[];
dir?: string;
hostIP: string;
protocol: string;
url: string;
}
export interface McpServerSearch extends ReqPage {
name: string;
}
export interface McpServerDelete {
id: number;
}
export interface McpServerOperate {
id: number;
operate: string;
}
export interface McpBindDomain {
domain: string;
sslID: number;
ipList: string;
}
export interface McpDomainRes {
domain: string;
sslID: number;
acmeAccountID: number;
allowIPs: string[];
websiteID?: number;
connUrl: string;
}
export interface McpBindDomainUpdate {
websiteID: number;
sslID: number;
ipList: string;
}
export interface ImportMcpServer {
name: string;
command: string;
ssePath: string;
containerName: string;
environments: Environment[];
}
} }
+4 -1
View File
@@ -2,7 +2,6 @@ export namespace Login {
export interface ReqLoginForm { export interface ReqLoginForm {
name: string; name: string;
password: string; password: string;
ignoreCaptcha: boolean;
captcha: string; captcha: string;
captchaID: string; captchaID: string;
authMethod: string; authMethod: string;
@@ -26,4 +25,8 @@ export namespace Login {
export interface ResAuthButtons { export interface ResAuthButtons {
[propName: string]: any; [propName: string]: any;
} }
export interface LoginSetting {
language: string;
needCaptcha: boolean;
}
} }
-3
View File
@@ -212,9 +212,6 @@ export namespace Container {
password: string; password: string;
auth: boolean; auth: boolean;
} }
export interface RepoDelete {
ids: Array<number>;
}
export interface RepoInfo { export interface RepoInfo {
id: number; id: number;
createdAt: Date; createdAt: Date;
+1
View File
@@ -81,6 +81,7 @@ export namespace Cronjob {
export interface CronjobDelete { export interface CronjobDelete {
ids: Array<number>; ids: Array<number>;
cleanData: boolean; cleanData: boolean;
cleanRemoteData: boolean;
} }
export interface UpdateStatus { export interface UpdateStatus {
id: number; id: number;
+32
View File
@@ -39,3 +39,35 @@ export const getBindDomain = (req: AI.BindDomainReq) => {
export const updateBindDomain = (req: AI.BindDomain) => { export const updateBindDomain = (req: AI.BindDomain) => {
return http.post(`/ai/domain/update`, req); return http.post(`/ai/domain/update`, req);
}; };
export const pageMcpServer = (req: AI.McpServerSearch) => {
return http.post<ResPage<AI.McpServer>>(`/ai/mcp/search`, req);
};
export const createMcpServer = (req: AI.McpServer) => {
return http.post(`/ai/mcp/server`, req);
};
export const updateMcpServer = (req: AI.McpServer) => {
return http.post(`/ai/mcp/server/update`, req);
};
export const deleteMcpServer = (req: AI.McpServerDelete) => {
return http.post(`/ai/mcp/server/del`, req);
};
export const operateMcpServer = (req: AI.McpServerOperate) => {
return http.post(`/ai/mcp/server/op`, req);
};
export const bindMcpDomain = (req: AI.McpBindDomain) => {
return http.post(`/ai/mcp/domain/bind`, req);
};
export const getMcpDomain = () => {
return http.get<AI.McpDomainRes>(`/ai/mcp/domain/get`);
};
export const updateMcpDomain = (req: AI.McpBindDomainUpdate) => {
return http.post(`/ai/mcp/domain/update`, req);
};
+2 -2
View File
@@ -21,8 +21,8 @@ export const checkIsDemo = () => {
return http.get<boolean>('/auth/demo'); return http.get<boolean>('/auth/demo');
}; };
export const getLanguage = () => { export const getAuthSetting = () => {
return http.get<string>(`/auth/language`); return http.get<Login.LoginSetting>(`/auth/setting`);
}; };
export const checkIsIntl = () => { export const checkIsIntl = () => {

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