Compare commits

..
Author SHA1 Message Date
zhengkunwang223 9366b8ff27 feat: add gb10 vllm image support 2026-08-28 16:57:02 +08:00
zhengkunwang223 ea52faab4b feat: add gb10 vllm image support 2026-08-27 18:36:04 +08:00
zhengkunwang223 2878979dfe feat: add gb10 vllm image support 2026-08-27 17:16:48 +08:00
385 changed files with 12223 additions and 31770 deletions
-40
View File
@@ -2,14 +2,11 @@ package v2
import (
"errors"
"net/http"
"net/url"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/gin-gonic/gin"
)
@@ -297,34 +294,6 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
return
}
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
switch {
case errors.Is(err, repo.ErrAlertConfigRevisionConflict):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_CONFLICT", "ErrInvalidParams", err)
case errors.Is(err, repo.ErrAlertConfigRevisionRequired):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_REQUIRED", "ErrInvalidParams", err)
default:
helper.InternalServer(c, err)
}
return
}
helper.Success(c)
}
// @Tags Alert
// @Summary Update alert config status
// @Accept json
// @Param request body dto.AlertConfigStatusUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/status [post]
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
var req dto.AlertConfigStatusUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -377,15 +346,6 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if req.Type == constant.Custom {
result, err := alertService.TestCustomAlertConfig(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
return
}
flag, err := alertService.TestAlertConfig(req)
if err != nil {
helper.InternalServer(c, err)
+1 -1
View File
@@ -439,7 +439,7 @@ func (b *BaseApi) ContainerItemStats(c *gin.Context) {
return
}
data, err := containerService.ContainerItemStats(c.Request.Context(), req)
data, err := containerService.ContainerItemStats(req)
if err != nil {
helper.InternalServer(c, err)
return
+1 -26
View File
@@ -684,35 +684,10 @@ func (b *BaseApi) StopWget(c *gin.Context) {
return
}
if err := files.CancelDownload(req.Key); err != nil {
helper.InternalServer(c, err)
return
}
files.CancelDownload(req.Key)
helper.Success(c)
}
// @Tags File
// @Summary Remove finished download progress records without deleting files
// @Accept json
// @Param request body request.FileProcessRemoveReq true "request"
// @Success 200 {object} response.FileProcessKeys
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/process/remove [post]
// @x-panel-log {"bodyKeys":["keys"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移除已结束下载记录 [keys]","formatEN":"Remove finished download records [keys]"}
func (b *BaseApi) RemoveWgetRecords(c *gin.Context) {
var req request.FileProcessRemoveReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
keys, err := files.RemoveDownloadRecords(req.Keys)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, response.FileProcessKeys{Keys: keys})
}
// @Tags File
// @Summary Move file
// @Accept json
+35 -184
View File
@@ -9,30 +9,10 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) UpdatePanelFirewallPort(c *gin.Context) {
if !global.IsMaster {
c.AbortWithStatus(http.StatusForbidden)
return
}
var request struct {
OldPort uint `json:"oldPort" validate:"required,min=1,max=65535"`
NewPort uint `json:"newPort" validate:"required,min=1,max=65535"`
}
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallService.UpdatePanelPort(c.Request.Context(), request.OldPort, request.NewPort); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Load firewall base info
// @Accept json
@@ -60,7 +40,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
// @Summary Operate firewall
// @Accept json
// @Param request body dto.FirewallLifecycleOperation true "request"
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/operate [post]
@@ -71,13 +51,12 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
return
}
result, err := firewallService.QueueFirewallOperation(request)
if err != nil {
if err := firewallService.OperateFirewall(request); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
// @Tags Firewall
@@ -122,7 +101,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
// @Summary Operate forwarding rules
// @Accept json
// @Param request body dto.ForwardRuleOperate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/forward/operate [post]
@@ -133,41 +112,33 @@ func (b *BaseApi) OperateForwardingRules(c *gin.Context) {
return
}
result, err := forwardingService.OperateRules(request)
if err != nil {
if err := forwardingService.OperateRules(request); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
// @Tags Firewall
// @Summary Enable forwarding
// @Accept json
// @Param request body dto.FirewallInitializationTask true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/forward/enable [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"初始化并启用端口转发","formatEN":"initialize and enable port forwarding"}
func (b *BaseApi) EnableForwarding(c *gin.Context) {
var request dto.FirewallInitializationTask
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := forwardingService.QueueInitialization(request)
if err != nil {
if err := forwardingService.Enable(); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
// @Tags Firewall
// @Summary Apply/Unload/Init firewall filter chain
// @Accept json
// @Param request body dto.FilterChainOperation true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/filter/operate [post]
@@ -177,20 +148,12 @@ func (b *BaseApi) OperateFilterChain(c *gin.Context) {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if request.Operate == "init-base" {
result, err := firewallService.QueueFilterChainInitialization(request)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
return
}
if err := firewallService.OperateFilterChain(request); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.FilterChainOperationResponse{})
helper.Success(c)
}
// @Tags Firewall
@@ -261,30 +224,29 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
}
// @Tags Firewall
// @Summary Adopt an external firewall rule
// @Summary Check unified firewall v2 rules for duplicates and conflicts
// @Accept json
// @Param request body dto.FirewallRuleAdopt true "request"
// @Success 200
// @Param request body dto.FirewallRuleCheck true "request"
// @Success 200 {object} dto.FirewallRuleCheckResponse
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/adopt [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
var request dto.FirewallRuleAdopt
// @Router /hosts/firewall/rules/check [post]
func (b *BaseApi) CheckFirewallRules(c *gin.Context) {
var request dto.FirewallRuleCheck
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
result, err := firewallService.Check(c.Request.Context(), c.ClientIP(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.Success(c)
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Queue firewall rule creation
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
// @Summary Create unified firewall v2 rules
// @Accept json
// @Param request body dto.FirewallRuleCreate true "request"
// @Success 200 {object} dto.FirewallRuleCreateResponse
@@ -368,8 +330,7 @@ func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
}
// @Tags Firewall
// @Summary Queue firewall rule deletion
// @Description Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.
// @Summary Delete managed unified firewall v2 rules
// @Accept json
// @Param request body dto.FirewallRuleDelete true "request"
// @Success 200 {object} dto.FirewallRuleDeleteResponse
@@ -456,18 +417,18 @@ func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
func handleFirewallRuleError(c *gin.Context, err error) {
switch {
case errors.Is(err, filter.ErrProtectedRule):
case errors.Is(err, filter.ErrLockoutRisk), errors.Is(err, filter.ErrProtectedRule):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrRuleStale):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrManagedScopeChange):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
case errors.Is(err, filter.ErrRuleCheckRequired):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_CHECK_REQUIRED", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation),
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrVerificationFailed):
@@ -492,72 +453,6 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary Create firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist [post]
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistCreate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Update firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist/update [post]
// @x-panel-log {"bodyKeys":["oldRule","rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"编辑防火墙端口白名单","formatEN":"update firewall port whitelist"}
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistUpdate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Delete firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist/delete [post]
// @x-panel-log {"bodyKeys":["rules"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙端口白名单","formatEN":"delete firewall port whitelist"}
func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistDelete
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Operate firewall backend
// @Accept json
@@ -573,16 +468,6 @@ func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
return
}
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) {
helper.ErrorWithBusinessCode(
c,
http.StatusConflict,
"FW_BACKEND_CLEANUP_REQUIRED",
"ErrInvalidParams",
err,
)
return
}
helper.InternalServer(c, err)
return
}
@@ -604,21 +489,6 @@ func (b *BaseApi) ListDockerPortGuard(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary List Docker published ports
// @Success 200 {array} dto.DockerPortGuardContainer
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/endpoints [get]
func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
data, err := dockerPortGuardService.LoadPublishedPorts(c.Request.Context())
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary Sync Docker port guard rules
// @Success 200
@@ -638,7 +508,7 @@ func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
// @Summary Operate Docker port guard
// @Accept json
// @Param request body dto.DockerPortGuardOperation true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/operate [post]
@@ -648,15 +518,6 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if request.Operation == "initialize" {
result, err := dockerPortGuardService.QueueInitialization(request)
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, result)
return
}
if err := dockerPortGuardService.Operate(c.Request.Context(), request); err != nil {
handleDockerPortGuardError(c, err)
return
@@ -668,7 +529,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
// @Summary Delete Docker port guard policies
// @Accept json
// @Param request body dto.DockerPortGuardPolicyBatchDelete true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/policies/delete/batch [post]
@@ -678,45 +539,35 @@ func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := dockerPortGuardService.DeletePolicies(request)
if err != nil {
if err := dockerPortGuardService.DeletePolicies(c.Request.Context(), request); err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
// @Tags Firewall
// @Summary Batch upsert Docker port guard policies
// @Accept json
// @Param request body dto.DockerPortGuardPolicyBatch true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/policies/batch [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略","formatEN":"batch update Docker port guard policies"}
// @x-panel-log {"bodyKeys":["mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略 [mode]","formatEN":"batch update Docker port guard policies [mode]"}
func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
var request dto.DockerPortGuardPolicyBatch
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := dockerPortGuardService.UpsertPolicies(request)
if err != nil {
if err := dockerPortGuardService.UpsertPolicies(c.Request.Context(), request); err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
func handleDockerPortGuardError(c *gin.Context, err error) {
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err)
return
}
if errors.Is(err, service.ErrDockerNftablesChainUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE", "ErrDockerNftablesChainUnavailable", err)
return
}
if errors.Is(err, service.ErrDockerGuardInvalid) {
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID", "ErrInvalidParams", err)
return
+31 -143
View File
@@ -1,14 +1,11 @@
package v2
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
@@ -22,35 +19,29 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
gossh "golang.org/x/crypto/ssh"
)
// @Tags Terminal
// @Summary Ws local terminal
// @Param command query string false "command"
// @Param session query string false "session id to reattach"
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/local [get]
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
b.runSSHSession(c, "local", loadLocalConn, c.DefaultQuery("command", ""))
b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
}
// @Tags Terminal
// @Summary Ws host SSH
// @Param id query integer false "id"
// @Param command query string false "command"
// @Param session query string false "session id to reattach"
// @Param title query string false "session title shown in the session list"
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/ssh [get]
func (b *BaseApi) WsHostSSH(c *gin.Context) {
b.runSSHSession(c, "ssh", func() (*ssh.SSHClient, error) {
b.runSSHSession(c, func() (*ssh.SSHClient, error) {
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
if hostID <= 0 {
return nil, errors.New("missing host id")
@@ -74,33 +65,26 @@ func (b *BaseApi) WsContainerTerminal(c *gin.Context) {
return
}
defer wsConn.Close()
identity, ok := loadTerminalIdentity(c)
if !ok {
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
slave, err := loadContainerTerminalCommand(c)
if wshandleError(wsConn, err) {
return
}
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return
}
opts := terminal.SessionOptions{
Identity: identity,
Kind: "container",
Target: containerTerminalTarget(c),
Cols: cols,
Rows: rows,
}
if err := terminal.ServeCommand(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*terminal.LocalCommand, error) {
return loadContainerTerminalCommand(c)
}); err != nil {
_ = wshandleError(wsConn, err)
}
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
func containerTerminalTarget(c *gin.Context) string {
query := c.Request.URL.Query()
for _, key := range []string{"cols", "rows", "session", "terminalRevalidate"} {
query.Del(key)
}
sum := sha256.Sum256([]byte(query.Encode()))
return hex.EncodeToString(sum[:])
<-quitChan
global.LOG.Info("websocket finished")
closeTerminalConn(wsConn)
}
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
@@ -131,128 +115,32 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
return wsConn, cols, rows, true
}
func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ssh.SSHClient, error), command string) {
func (b *BaseApi) runSSHSession(c *gin.Context, connect func() (*ssh.SSHClient, error), command string) {
wsConn, cols, rows, ok := prepareTerminalSession(c)
if !ok {
return
}
defer wsConn.Close()
identity, ok := loadTerminalIdentity(c)
if !ok {
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
return
}
hostID := 0
if kind == "ssh" {
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0"))
client, clientErr := connect()
if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) {
return
}
opts := terminal.SessionOptions{
Identity: identity,
Kind: kind,
Title: sanitizeTerminalTitle(c.Query("title")),
Persistent: c.Query("terminalPersistent") == "true",
HostID: uint(max(hostID, 0)),
Cols: cols,
Rows: rows,
InitCmd: command,
}
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
client, err := connect()
if err != nil {
return nil, errors.WithMessage(err, "failed to set up the connection. Please check the host information")
}
return client.Client, nil
})
if err != nil {
_ = wshandleError(wsConn, err)
}
}
defer client.Close()
// @Tags Terminal
// @Summary List the caller's live terminal sessions
// @Success 200 {array} terminal.Info
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/sessions/search [post]
func (b *BaseApi) SearchTerminalSessions(c *gin.Context) {
identity, ok := loadTerminalIdentity(c)
if !ok {
helper.BadRequest(c, errors.New("missing terminal identity"))
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
if wshandleError(wsConn, err) {
return
}
helper.SuccessWithData(c, terminal.List(identity))
}
defer sws.Close()
// @Tags Terminal
// @Summary Close a terminal session
// @Accept json
// @Param request body dto.TerminalSessionClose true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/sessions/close [post]
func (b *BaseApi) CloseTerminalSession(c *gin.Context) {
var req dto.TerminalSessionClose
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
identity, ok := loadTerminalIdentity(c)
if !ok {
helper.BadRequest(c, errors.New("missing terminal identity"))
return
}
if err := terminal.CloseSession(req.ID, identity); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
quitChan := make(chan bool, 3)
sws.Start(quitChan)
go sws.Wait(quitChan)
// @Tags Terminal
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/sessions/closeAll [post]
func (b *BaseApi) CloseAllTerminalSessions(c *gin.Context) {
identity, ok := loadTerminalIdentity(c)
if !ok {
helper.BadRequest(c, errors.New("missing terminal identity"))
return
}
terminal.Revoke("auth_session", identity.UserID, identity.AuthSessionID)
helper.Success(c)
}
<-quitChan
func (b *BaseApi) RevokeTerminalSessions(c *gin.Context) {
var req dto.TerminalSessionRevoke
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if (req.Scope == "auth_session" && (req.UserID == "" || req.AuthSessionID == "")) ||
(req.Scope == "user" && req.UserID == "") {
helper.BadRequest(c, errors.New("missing terminal revocation identity"))
return
}
terminal.Revoke(req.Scope, req.UserID, req.AuthSessionID)
helper.Success(c)
}
func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
identity := terminal.Identity{
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
}
return identity, identity.Valid()
}
// sanitizeTerminalTitle keeps the title a short single line.
func sanitizeTerminalTitle(title string) string {
title = strings.Join(strings.Fields(title), " ")
if r := []rune(title); len(r) > 64 {
title = string(r[:64])
}
return title
closeTerminalConn(wsConn)
}
func closeTerminalConn(wsConn *websocket.Conn) {
+7 -16
View File
@@ -162,25 +162,16 @@ type AgentWebsiteBindReq struct {
}
type AgentModelConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
AccountID uint `json:"accountId" validate:"required"`
Model string `json:"model" validate:"required"`
Fallbacks []string `json:"fallbacks"`
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
AgentID uint `json:"agentId" validate:"required"`
AccountID uint `json:"accountId" validate:"required"`
Model string `json:"model" validate:"required"`
Fallbacks []string `json:"fallbacks"`
}
type AgentModelConfig struct {
AccountID uint `json:"accountId"`
Model string `json:"model"`
Fallbacks []string `json:"fallbacks"`
Metadata []AgentModelMetadata `json:"metadata"`
}
type AgentModelMetadata struct {
Model string `json:"model" validate:"required"`
InputMode string `json:"inputMode" validate:"required,oneof=auto text image"`
ContextWindow int `json:"contextWindow" validate:"min=0"`
MaxTokens int `json:"maxTokens" validate:"min=0"`
AccountID uint `json:"accountId"`
Model string `json:"model"`
Fallbacks []string `json:"fallbacks"`
}
type AgentHermesChatSessionItem struct {
+16 -29
View File
@@ -151,25 +151,16 @@ type AlertLog struct {
}
type AlertDetail struct {
LicenseId string `json:"licenseId"`
Type string `json:"type"`
SubType string `json:"subType"`
Title string `json:"title"`
Method string `json:"method"`
LicenseCode string `json:"licenseCode"`
DeviceId string `json:"deviceId"`
Project string `json:"project"`
Params []Param `json:"params"`
Phone string `json:"phone"`
Task *AlertTaskMetadata `json:"task,omitempty"`
}
type AlertTaskMetadata struct {
AlertID uint `json:"alertId"`
Type string `json:"type"`
Quota string `json:"quota"`
QuotaType string `json:"quotaType"`
Method string `json:"method"`
LicenseId string `json:"licenseId"`
Type string `json:"type"`
SubType string `json:"subType"`
Title string `json:"title"`
Method string `json:"method"`
LicenseCode string `json:"licenseCode"`
DeviceId string `json:"deviceId"`
Project string `json:"project"`
Params []Param `json:"params"`
Phone string `json:"phone"`
}
type AlertRule struct {
@@ -304,19 +295,15 @@ type OfflineQueryRequest struct {
}
type AlertConfigUpdate struct {
ID uint `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Status string `json:"status"`
Config string `json:"config"`
DisplayName string `json:"displayName"`
Revision *time.Time `json:"revision"`
ID uint `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Status string `json:"status"`
Config string `json:"config"`
DisplayName string `json:"displayName"`
}
type AlertConfigTest struct {
ID uint `json:"id"`
Type string `json:"type"`
Config string `json:"config"`
Host string `json:"host"`
Port int `json:"port"`
Sender string `json:"sender"`
-80
View File
@@ -1,80 +0,0 @@
package dto
const AlertCustomWebhookSchemaVersion = 1
type AlertConfigStatusUpdate struct {
ID uint `json:"id" validate:"required"`
Status string `json:"status" validate:"required,oneof=Enable Disable"`
}
type AlertCustomWebhookSecretMutation struct {
Action string `json:"action,omitempty"`
Value string `json:"value,omitempty"`
}
type AlertCustomWebhookURL struct {
AlertCustomWebhookSecretMutation
Configured bool `json:"configured"`
Masked string `json:"masked,omitempty"`
}
type AlertCustomWebhookBody struct {
Type string `json:"type"`
Template string `json:"template,omitempty"`
Fields []AlertCustomWebhookFormField `json:"fields,omitempty"`
}
type AlertCustomWebhookFormField struct {
Key string `json:"key"`
Value string `json:"value"`
}
type AlertCustomWebhookHeader struct {
UID string `json:"uid"`
Key string `json:"key"`
Secret bool `json:"secret"`
Action string `json:"action,omitempty"`
Value string `json:"value,omitempty"`
Configured bool `json:"configured,omitempty"`
Masked string `json:"masked,omitempty"`
}
type AlertCustomWebhookConfig struct {
SchemaVersion int `json:"schemaVersion"`
State string `json:"state,omitempty"`
DisplayName string `json:"displayName"`
Preset string `json:"preset"`
Method string `json:"method"`
URL AlertCustomWebhookURL `json:"url"`
Body AlertCustomWebhookBody `json:"body"`
Headers []AlertCustomWebhookHeader `json:"headers"`
}
type AlertCustomWebhookSecretConfig struct {
SchemaVersion int `json:"schemaVersion"`
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
}
type AlertCustomWebhookResolvedConfig struct {
SchemaVersion int
DisplayName string
Preset string
Method string
URL string
Body AlertCustomWebhookBody
Headers []AlertCustomWebhookResolvedHeader
}
type AlertCustomWebhookResolvedHeader struct {
Key string
Value string
}
type AlertConfigTestResult struct {
Success bool `json:"success"`
StatusCode int `json:"statusCode,omitempty"`
Duration int64 `json:"duration,omitempty"` // milliseconds
Message string `json:"message,omitempty"`
Response string `json:"response,omitempty"`
}
+65 -130
View File
@@ -1,7 +1,6 @@
package dto
import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
)
@@ -17,9 +16,7 @@ type FirewallSubsystemStatus struct {
Version string `json:"version"`
PingStatus string `json:"pingStatus"`
Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"`
SyncError string `json:"syncError,omitempty"`
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
}
@@ -29,11 +26,6 @@ type FirewallLifecycleOperation struct {
WithDockerRestart bool `json:"withDockerRestart"`
}
type FirewallLifecycleOperationResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued"`
}
type FirewallBackendOption struct {
Name string `json:"name"`
Installed bool `json:"installed"`
@@ -41,7 +33,6 @@ type FirewallBackendOption struct {
Initialized bool `json:"initialized"`
Bound bool `json:"bound"`
Supported bool `json:"supported"`
SupportReason string `json:"supportReason,omitempty"`
Implementation string `json:"implementation,omitempty"`
Message string `json:"message,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
@@ -62,26 +53,11 @@ type FirewallBackendGroup struct {
}
type FirewallSettings struct {
System FirewallBackendGroup `json:"system"`
Forwarding FirewallBackendGroup `json:"forwarding"`
Docker FirewallBackendGroup `json:"docker"`
PingStatus string `json:"pingStatus"`
PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
PanelPort string `json:"panelPort"`
SSHPort string `json:"sshPort"`
}
type FirewallPortWhitelistCreate struct {
Rule filter.PortWhitelist `json:"rule" validate:"required"`
}
type FirewallPortWhitelistUpdate struct {
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
Rule filter.PortWhitelist `json:"rule" validate:"required"`
}
type FirewallPortWhitelistDelete struct {
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
System FirewallBackendGroup `json:"system"`
Forwarding FirewallBackendGroup `json:"forwarding"`
Docker FirewallBackendGroup `json:"docker"`
PingStatus string `json:"pingStatus"`
PortWhitelist string `json:"portWhiteList"`
}
type FirewallBackendOperation struct {
@@ -93,28 +69,17 @@ type FirewallBackendOperation struct {
type FilterChainOperation struct {
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FilterChainOperationResponse struct {
TaskID string `json:"taskID"`
Queued bool `json:"queued"`
type FirewallSystemPort struct {
Family string
Port string
Protocol string
}
type FirewallInitializationTask struct {
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallSystemPort = firewall.SystemPort
type FirewallRuleInventoryResponse struct {
IPv4Range filter.PositionRange `json:"ipv4Range"`
IPv6Range filter.PositionRange `json:"ipv6Range"`
Total int64 `json:"total"`
AllTotal int64 `json:"allTotal"`
ManagedTotal int64 `json:"managedTotal"`
Items []filter.InventoryItem `json:"items"`
Notices []filter.ScopeNotice `json:"notices,omitempty"`
Items []filter.InventoryItem `json:"items"`
Notices []filter.ScopeNotice `json:"notices,omitempty"`
}
type FirewallRuleResetResponse struct {
@@ -123,21 +88,23 @@ type FirewallRuleResetResponse struct {
}
type FirewallRuleReset struct {
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
WithDockerRestart bool `json:"withDockerRestart"`
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
}
type FirewallRuleCheckResult struct {
Decision filter.CheckDecision `json:"decision"`
Classification filter.CheckClassification `json:"classification"`
Reason string `json:"reason"`
RequestedRule filter.FirewallRule `json:"requestedRule"`
RequestedRuleKey string `json:"requestedRuleKey"`
ExistingRuleUUID string `json:"existingRuleUUID,omitempty"`
Candidates []filter.ObservedRule `json:"candidates,omitempty"`
AllowedActions []filter.CheckAction `json:"allowedActions,omitempty"`
CheckFlag string `json:"checkFlag"`
}
type FirewallRuleInventory struct {
Refresh bool `json:"refresh,omitempty"`
PageInfo
Scope filter.Scope `json:"scope,omitempty"`
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
All bool `json:"all,omitempty"`
Info string `json:"info"`
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
Scope filter.Scope `json:"scope" validate:"required"`
}
type FirewallNativeDetail struct {
@@ -168,26 +135,20 @@ type DockerPortGuardFamilyStatus struct {
}
type DockerPortGuardEndpoint struct {
Family string `json:"family"`
HostIP string `json:"hostIP"`
HostPort uint16 `json:"hostPort"`
Protocol string `json:"protocol"`
ContainerID string `json:"containerID"`
ContainerName string `json:"containerName"`
ContainerState string `json:"containerState,omitempty"`
ContainerPort uint16 `json:"containerPort"`
Compose string `json:"compose,omitempty"`
Application string `json:"application,omitempty"`
PolicyUUID string `json:"policyUUID,omitempty"`
Mode string `json:"mode,omitempty"`
NativeAction string `json:"nativeAction,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Sources []string `json:"sources"`
Effective bool `json:"effective"`
Description string `json:"description,omitempty"`
TrafficPath string `json:"trafficPath"`
ManagementTarget string `json:"managementTarget"`
ManagementReason string `json:"managementReason,omitempty"`
Family string `json:"family"`
HostIP string `json:"hostIP"`
HostPort uint16 `json:"hostPort"`
Protocol string `json:"protocol"`
ContainerID string `json:"containerID"`
ContainerName string `json:"containerName"`
ContainerPort uint16 `json:"containerPort"`
Compose string `json:"compose,omitempty"`
Application string `json:"application,omitempty"`
PolicyUUID string `json:"policyUUID,omitempty"`
Mode string `json:"mode,omitempty"`
Sources []string `json:"sources"`
Effective bool `json:"effective"`
Description string `json:"description,omitempty"`
}
type DockerPortGuardPortGroup struct {
@@ -220,43 +181,47 @@ type DockerPortGuardEndpointIdentity struct {
}
type DockerPortGuardPolicyBatch struct {
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
Endpoints []DockerPortGuardEndpointIdentity `json:"endpoints" validate:"required,min=1,max=256,dive"`
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
Sources []string `json:"sources" validate:"max=256,dive,required,max=64"`
Description string `json:"description" validate:"max=256"`
}
type DockerPortGuardPolicyBatchDelete struct {
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
}
type DockerPortGuardPolicy struct {
DockerPortGuardEndpointIdentity
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
Sources []string `json:"sources" validate:"dive,required,max=64"`
Description string `json:"description" validate:"max=256"`
UUIDs []string `json:"uuids" validate:"required,min=1,max=256,dive,required,max=64"`
}
type DockerPortGuardOperation struct {
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallRuleAdopt struct {
Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
type FirewallRuleCheckItem struct {
UUID string `json:"uuid" validate:"omitempty,max=64"`
Rule filter.FirewallRule `json:"rule" validate:"required"`
}
type FirewallRuleCheck struct {
Items []FirewallRuleCheckItem `json:"items" validate:"required,min=1,max=256,dive"`
}
type FirewallRuleCheckResponse struct {
Items []FirewallRuleCheckResult `json:"items"`
}
type FirewallRuleCreateItem struct {
Rule filter.FirewallRule `json:"rule" validate:"required"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
SourceID string `json:"sourceID"`
Rule filter.FirewallRule `json:"rule" validate:"required"`
CheckFlag string `json:"checkFlag"`
Action filter.CheckAction `json:"action"`
AdoptInstanceKey string `json:"adoptInstanceKey"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
SourceID string `json:"sourceID"`
}
type FirewallRuleCreate struct {
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,max=256,dive"`
}
type FirewallRuleCreateResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued,omitempty"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
@@ -328,18 +293,10 @@ type FirewallRuleSyncFailure struct {
}
type FirewallRuleDelete struct {
UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"`
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
}
type FirewallRuleDeleteTarget struct {
Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
UUIDs []string `json:"uuids" validate:"required,min=1,max=256,dive,required,max=64"`
}
type FirewallRuleDeleteResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued,omitempty"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
@@ -352,11 +309,8 @@ type FirewallRuleDeleteFailure struct {
}
type FirewallRuleUpdate struct {
UUID string `json:"uuid" validate:"required,max=64"`
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
Priority *int `json:"priority,omitempty" validate:"excluded_with=Rule OrderIndex"`
UUID string `json:"uuid" validate:"required,max=64"`
Rule filter.FirewallRule `json:"rule" validate:"required"`
}
type FirewallRuleReorder struct {
@@ -364,22 +318,3 @@ type FirewallRuleReorder struct {
TargetPosition *int64 `json:"targetPosition"`
Priority *int `json:"priority"`
}
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
p.Items = append(p.Items, item)
switch item.Status {
case firewallsync.StatusReady:
p.Ready++
p.Total++
case firewallsync.StatusExisting:
p.Existing++
p.Total++
case firewallsync.StatusRemove:
p.Removed++
case firewallsync.StatusBlocked:
p.Blocked++
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
p.Total++
}
}
}
+3 -2
View File
@@ -2,12 +2,13 @@ package dto
type ForwardRuleSearch struct {
PageInfo
All bool `json:"all,omitempty"`
Info string `json:"info"`
Status string `json:"status"`
Strategy string `json:"strategy"`
}
// ForwardRule preserves the existing firewall search response shape while
// keeping forwarding data separate from the filter client model.
type ForwardRule struct {
ID uint `json:"id"`
Chain string `json:"chain"`
@@ -32,7 +33,7 @@ type ForwardRule struct {
type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"`
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
Rules []ForwardRuleOperation `json:"rules"`
}
type ForwardRuleOperation struct {
+1 -7
View File
@@ -51,19 +51,13 @@ const (
CACHE NginxKey = "cache"
HttpPer NginxKey = "http-per"
ProxyCache NginxKey = "proxy-cache"
Brotli NginxKey = "brotli"
)
// BrotliKeys are served from the panel-managed http.d file rather than
// nginx.conf, because the module is optional: its directives must disappear
// together with the module, otherwise nginx refuses to start.
var BrotliKeys = []string{"brotli", "brotli_comp_level", "brotli_min_length", "brotli_types"}
var ScopeKeyMap = map[NginxKey][]string{
Index: {"index"},
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
SSL: {"ssl_certificate", "ssl_certificate_key"},
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level", "gzip_types", "gzip_vary", "gzip_proxied"},
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"},
}
var StaticFileKeyMap = map[NginxKey]struct {
-5
View File
@@ -122,7 +122,6 @@ type FileWget struct {
Name string `json:"name" validate:"required"`
IgnoreCertificate bool `json:"ignoreCertificate"`
UseProxy bool `json:"useProxy"`
UseServerFilename bool `json:"useServerFilename"`
}
type FileMove struct {
@@ -159,10 +158,6 @@ type FileProcessReq struct {
Key string `json:"key"`
}
type FileProcessRemoveReq struct {
Keys []string `json:"keys" validate:"required,min=1,max=1000"`
}
type FileRoleUpdate struct {
Path string `json:"path" validate:"required"`
User string `json:"user" validate:"required"`
+8 -9
View File
@@ -66,15 +66,14 @@ type RuntimeDelete struct {
}
type RuntimeUpdate struct {
AppDetailID uint `json:"appDetailId"`
Name string `json:"name"`
ID uint `json:"id"`
Image string `json:"image"`
Version string `json:"version"`
Rebuild bool `json:"rebuild"`
Source string `json:"source"`
CodeDir string `json:"codeDir"`
Remark string `json:"remark"`
Name string `json:"name"`
ID uint `json:"id"`
Image string `json:"image"`
Version string `json:"version"`
Rebuild bool `json:"rebuild"`
Source string `json:"source"`
CodeDir string `json:"codeDir"`
Remark string `json:"remark"`
Params map[string]interface{} `json:"params"`
NodeConfig
-11
View File
@@ -17,17 +17,6 @@ type NginxParam struct {
Params []string `json:"params"`
}
// NginxBrotliRes carries the brotli settings together with where they live.
// ManagedExternally is true when the user defined brotli by hand, in which
// case the panel only reports the values and must not write its own copy.
// ManagedUnavailable is true when the panel could not wire the managed
// configuration into nginx.conf at all, so the reported values are inert.
type NginxBrotliRes struct {
Params []NginxParam `json:"params"`
ManagedExternally bool `json:"managedExternally"`
ManagedUnavailable bool `json:"managedUnavailable"`
}
type NginxAuthRes struct {
Enable bool `json:"enable"`
Items []dto.NginxAuth `json:"items"`
+1 -1
View File
@@ -35,7 +35,7 @@ type SettingUpdate struct {
}
type AgentSettingUpdate struct {
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin"`
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin FirewallPortWhiteList"`
Value string `json:"value"`
}
-11
View File
@@ -1,11 +0,0 @@
package dto
type TerminalSessionClose struct {
ID string `json:"id" validate:"required"`
}
type TerminalSessionRevoke struct {
Scope string `json:"scope" validate:"required,oneof=auth_session user all"`
UserID string `json:"userId"`
AuthSessionID string `json:"authSessionId"`
}
+10 -27
View File
@@ -1,12 +1,5 @@
package model
import (
"strings"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Alert struct {
BaseModel
@@ -25,11 +18,10 @@ type Alert struct {
type AlertTask struct {
BaseModel
Type string `gorm:"type:varchar(64);not null" json:"type"`
Quota string `gorm:"type:varchar(64)" json:"quota"`
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
DeliveryLogID *uint `gorm:"uniqueIndex" json:"-"`
Type string `gorm:"type:varchar(64);not null" json:"type"`
Quota string `gorm:"type:varchar(64)" json:"quota"`
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
}
type AlertLog struct {
@@ -49,21 +41,12 @@ type AlertLog struct {
type AlertConfig struct {
BaseModel
UID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"uid"`
Type string `gorm:"type:varchar(64);not null" json:"type"`
Title string `gorm:"type:varchar(64);not null" json:"title"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Config string `gorm:"type:text;not null" json:"config"`
SecretConfig string `gorm:"type:text;not null;default:''" json:"-"`
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
}
func (a *AlertConfig) BeforeCreate(_ *gorm.DB) error {
if strings.TrimSpace(a.UID) == "" {
a.UID = uuid.NewString()
}
return nil
Type string `gorm:"type:varchar(64);not null" json:"type"`
Title string `gorm:"type:varchar(64);not null" json:"title"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Config string `gorm:"type:varchar(256);not null" json:"config"`
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
}
type LoginLog struct {
+21 -16
View File
@@ -16,18 +16,14 @@ const FirewallRuleSequenceStep int64 = 1 << 32
type DockerPortGuardPolicy struct {
BaseModel
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"`
ReadOnly bool `gorm:"not null;default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
Mode string `gorm:"size:32;not null" json:"mode"`
Sources string `gorm:"type:text" json:"-"`
Description string `gorm:"type:text" json:"description"`
NativeAction string `gorm:"size:32;not null;default:''" json:"-"`
NativeRules string `gorm:"type:text" json:"-"`
Sequence int64 `gorm:"not null;default:0" json:"-"`
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"`
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
Mode string `gorm:"size:32;not null" json:"mode"`
Sources string `gorm:"type:text" json:"-"`
Description string `gorm:"type:text" json:"description"`
}
type ForwardingRule struct {
@@ -72,6 +68,19 @@ func FirewallRuleOwner(sourceKind, sourceID string) string {
return sourceKind + ":" + sourceID
}
func FirewallRulesRevision(rules []FirewallRule) (string, error) {
ordered := append([]FirewallRule(nil), rules...)
sort.Slice(ordered, func(i, j int) bool {
return ordered[i].UUID < ordered[j].UUID
})
payload, err := json.Marshal(ordered)
if err != nil {
return "", err
}
sum := sha256.Sum256(payload)
return hex.EncodeToString(sum[:]), nil
}
func FirewallRuleFromDomain(rule filter.FirewallRule) (FirewallRule, error) {
normalized, err := filter.NormalizeRule(rule)
if err != nil {
@@ -135,10 +144,6 @@ func (rule FirewallRule) RulesForProvider(provider filter.Provider) ([]filter.Fi
Interface: rule.Interface, ConnectionStates: connectionStates,
Action: filter.Action(rule.Action), Description: rule.Description,
}
if provider != filter.ProviderUFW && strings.EqualFold(strings.TrimSpace(base.Protocol), "all") &&
strings.TrimSpace(base.SourcePort) == "" && strings.TrimSpace(base.DestinationPort) != "" {
base.Protocol = "tcp/udp"
}
if provider == filter.ProviderFirewalld {
base.Priority = rule.Priority
}
+1 -11
View File
@@ -40,21 +40,12 @@ type Meta struct {
var catalog = map[string]Meta{
"custom": {
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "dashscope-images", "openai-embeddings"),
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
},
"ollama": {
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"),
},
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
"llmman": {
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
APIConfigs: []APIConfig{
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
{APIType: "openai-embeddings", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
},
},
"vllm": {
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
@@ -462,7 +453,6 @@ var legacyModelPrefixes = map[string][]string{
"custom": {"custom"},
"vllm": {"custom"},
"ollama": {"ollama"},
"llmman": {"llmman"},
"deepseek": {"deepseek"},
"bailian-coding-plan": {"bailian-coding-plan"},
"ark-coding-plan": {"ark-coding-plan"},
+2 -2
View File
@@ -43,8 +43,8 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
providerKey = "moonshot"
resolvedAPIType = "openai-completions"
usesBearer = false
case "ollama", "llmman":
apiKey = provider
case "ollama":
apiKey = "ollama"
usesBearer = false
case "openai", "openrouter", "anthropic":
preserveQualifiedModel = strings.Contains(modelName, "/")
+6 -9
View File
@@ -27,14 +27,11 @@ type verifyErrorResponse struct {
Message string `json:"message"`
}
const (
defaultVerifyTimeout = 30 * time.Second
defaultVerifyMaxTokens = 16
)
const defaultVerifyTimeout = 30 * time.Second
func SkipVerification(provider string) bool {
switch provider {
case "vllm", "ollama", "llmman", "kimi-coding":
case "vllm", "ollama", "kimi-coding":
return true
default:
return false
@@ -119,20 +116,20 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
}
headers["anthropic-version"] = "2023-06-01"
request.Body = mustJSON(map[string]interface{}{
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
"model": model, "max_tokens": 1, "stream": false,
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
})
case "openai-responses":
request.URL = baseURL + "/responses"
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": defaultVerifyMaxTokens, "stream": false})
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false})
default:
request.URL = baseURL + "/chat/completions"
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
headers["Authorization"] = "Bearer " + apiKey
}
request.Body = mustJSON(map[string]interface{}{
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false,
})
}
return request
+9 -217
View File
@@ -1,30 +1,20 @@
package repo
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/google/uuid"
"google.golang.org/genproto/googleapis/type/date"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"strconv"
"time"
)
type AlertRepo struct{}
var (
ErrAlertConfigRevisionConflict = errors.New("alert config revision conflict")
ErrAlertConfigRevisionRequired = errors.New("alert config revision is required")
)
type IAlertRepo interface {
WithByType(alertType string) DBOption
WithByStatusIn(status []string) DBOption
@@ -34,7 +24,6 @@ type IAlertRepo interface {
WithByCreateAt(date *date.Date) DBOption
WithByLicenseId(licenseId string) DBOption
WithByRecordId(recordId uint) DBOption
WithByDeliveryLogID(logID uint) DBOption
WithByAlertMethodContainsConfigID(id uint) DBOption
WithByMethodConfigIDs(ids []uint) DBOption
@@ -56,8 +45,6 @@ type IAlertRepo interface {
CleanAlertLogs() error
CreateAlertTask(alertTaskBase *model.AlertTask) error
CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error)
FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error)
DeleteAlertTask(opts ...DBOption) error
GetAlertTask(opts ...DBOption) (model.AlertTask, error)
LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
@@ -68,7 +55,6 @@ type IAlertRepo interface {
GetConfigById(id uint) (model.AlertConfig, error)
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error
CreateAlertConfig(config *model.AlertConfig) error
DeleteAlertConfig(opts ...DBOption) error
@@ -237,78 +223,13 @@ func (a *AlertRepo) DeleteLog(opts ...DBOption) error {
}
func (a *AlertRepo) CleanAlertLogs() error {
return global.AlertDB.Where("status <> ?", constant.AlertPushing).Delete(&model.AlertLog{}).Error
return global.AlertDB.Where("1 = 1").Delete(&model.AlertLog{}).Error
}
func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error {
return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).Error
}
func (a *AlertRepo) CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error) {
if alertTask == nil {
return false, fmt.Errorf("pending alert task is required")
}
created := false
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
var log model.AlertLog
if err := tx.Where("id = ? AND status = ?", logID, constant.AlertPushing).First(&log).Error; err != nil {
return err
}
if log.AlertId != alertID || log.Type != alertTask.Type || log.Method != alertTask.Method {
return fmt.Errorf("pending alert task does not match delivery log %d", logID)
}
alertTask.DeliveryLogID = &logID
result := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "delivery_log_id"}},
DoNothing: true,
}).Create(alertTask)
if result.Error != nil {
return result.Error
}
created = result.RowsAffected > 0
return nil
})
return created, err
}
func (a *AlertRepo) FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error) {
finalized := false
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
status := constant.AlertError
if succeeded {
status = constant.AlertSuccess
message = ""
}
result := tx.Model(&model.AlertLog{}).
Where("id = ? AND status = ?", logID, constant.AlertPushing).
Updates(map[string]interface{}{"status": status, "message": message})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return nil
}
finalized = true
if !succeeded {
return tx.Where("delivery_log_id = ?", logID).Delete(&model.AlertTask{}).Error
}
var count int64
if err := tx.Model(&model.AlertTask{}).Where("delivery_log_id = ?", logID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
if fallback == nil {
return fmt.Errorf("pending alert task metadata is unavailable for delivery log %d", logID)
}
fallback.DeliveryLogID = &logID
return tx.Create(fallback).Error
})
return finalized, err
}
func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error {
db, _ := getAlertDB(opts...)
return db.Delete(&model.AlertTask{}).Error
@@ -389,23 +310,7 @@ func (a *AlertRepo) UpdateAlertConfig(maps map[string]interface{}, opts ...DBOpt
return db.Model(&model.AlertConfig{}).Updates(maps).Error
}
func (a *AlertRepo) UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error {
if revision == nil {
return a.UpdateAlertConfig(maps, opts...)
}
db, _ := getAlertDB(opts...)
result := db.Model(&model.AlertConfig{}).Where("updated_at = ?", *revision).Updates(maps)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrAlertConfigRevisionConflict
}
return nil
}
func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error {
ensureAlertConfigUID(config)
return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
}
@@ -433,12 +338,6 @@ func (a *AlertRepo) WithByTypeNotIn(types []string) DBOption {
}
}
func (a *AlertRepo) WithByDeliveryLogID(logID uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("delivery_log_id = ?", logID)
}
}
func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
var configs []model.AlertConfig
db := global.AlertDB.Model(&model.AlertConfig{})
@@ -479,44 +378,26 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
return err
}
oldConfigMap := make(map[string]model.AlertConfig)
oldConfigByUID := make(map[string]model.AlertConfig)
oldConfigMap := make(map[string]uint)
oldConfigByType := make(map[string][]model.AlertConfig)
oldConfigByKey := make(map[string][]model.AlertConfig)
consumedConfigIDs := make(map[uint]struct{})
for _, item := range oldConfigs {
if strings.TrimSpace(item.UID) != "" {
oldConfigByUID[item.UID] = item
}
if singletonTypes[item.Type] {
oldConfigMap[item.Type] = item
oldConfigMap[item.Type] = item.ID
continue
}
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
}
for _, item := range data {
if uid := strings.TrimSpace(item.UID); uid != "" {
if matched, ok := oldConfigByUID[uid]; ok && matched.Type != item.Type {
tx.Rollback()
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", uid, matched.Type, item.Type)
}
}
if singletonTypes[item.Type] {
if matched, ok := oldConfigMap[item.Type]; ok {
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
tx.Rollback()
return err
}
if val, ok := oldConfigMap[item.Type]; ok {
item.ID = val
delete(oldConfigMap, item.Type)
consumedConfigIDs[item.ID] = struct{}{}
} else {
item.ID = 0
ensureAlertConfigUID(&item)
if err := validateAlertConfigSyncSecret(&item); err != nil {
tx.Rollback()
return err
}
}
if item.ID == 0 {
if err := tx.Create(&item).Error; err != nil {
@@ -530,31 +411,9 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
continue
}
if strings.TrimSpace(item.UID) != "" {
if matched, ok := oldConfigByUID[item.UID]; ok {
delete(oldConfigByUID, item.UID)
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
tx.Rollback()
return err
}
consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil {
tx.Rollback()
return err
}
deleteAlertConfigByID(oldConfigByType, matched.ID)
deleteAlertConfigByID(oldConfigByKey, matched.ID)
continue
}
}
key := alertConfigSyncKey(item)
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
delete(oldConfigByUID, matched.UID)
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
tx.Rollback()
return err
}
item.ID = matched.ID
consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil {
tx.Rollback()
@@ -565,12 +424,7 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
}
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
delete(oldConfigByUID, matched.UID)
deleteAlertConfigByID(oldConfigByKey, matched.ID)
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
tx.Rollback()
return err
}
item.ID = matched.ID
consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil {
tx.Rollback()
@@ -580,11 +434,6 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
}
item.ID = 0
ensureAlertConfigUID(&item)
if err := validateAlertConfigSyncSecret(&item); err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&item).Error; err != nil {
tx.Rollback()
return err
@@ -609,63 +458,6 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
return nil
}
func ensureAlertConfigUID(config *model.AlertConfig) {
if config != nil && strings.TrimSpace(config.UID) == "" {
config.UID = uuid.NewString()
}
}
func inheritAlertConfigSyncState(incoming *model.AlertConfig, existing model.AlertConfig) error {
if incoming.Type != existing.Type {
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", incoming.UID, existing.Type, incoming.Type)
}
preserveExistingCustom := incoming.Type == constant.Custom &&
existing.Status == constant.AlertDisable &&
incoming.Title == existing.Title &&
incoming.Status == existing.Status &&
incoming.Config == existing.Config &&
(incoming.SecretConfig == "" || incoming.SecretConfig == existing.SecretConfig)
incoming.ID = existing.ID
if strings.TrimSpace(incoming.UID) == "" {
incoming.UID = existing.UID
}
if incoming.Type == constant.Custom && incoming.SecretConfig == "" {
incoming.SecretConfig = existing.SecretConfig
}
if preserveExistingCustom {
return nil
}
return validateAlertConfigSyncSecret(incoming)
}
func validateAlertConfigSyncSecret(incoming *model.AlertConfig) error {
if incoming.Type != constant.Custom {
incoming.SecretConfig = ""
return nil
}
if strings.TrimSpace(incoming.SecretConfig) == "" {
return fmt.Errorf("custom webhook sync secret is missing")
}
var version struct {
SchemaVersion int `json:"schemaVersion"`
}
if err := json.Unmarshal([]byte(incoming.Config), &version); err != nil || version.SchemaVersion != 1 {
return fmt.Errorf("custom webhook sync config must use schemaVersion 1")
}
secret := incoming.SecretConfig
for _, prefix := range []string{"core:v1:", "agent:v1:"} {
if !strings.HasPrefix(secret, prefix) {
continue
}
ciphertext, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, prefix))
if err != nil || len(ciphertext) < 32 || len(ciphertext)%16 != 0 {
return fmt.Errorf("custom webhook sync secret envelope is invalid")
}
return nil
}
return fmt.Errorf("custom webhook sync secret must use a versioned envelope")
}
func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
var alerts []model.Alert
if err := tx.Select("method").Find(&alerts).Error; err != nil {
+5 -38
View File
@@ -10,47 +10,30 @@ import (
)
type IDockerPortGuardRepo interface {
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
ListRuntimeReadOnly(context.Context) ([]model.DockerPortGuardPolicy, error)
List(context.Context) ([]model.DockerPortGuardPolicy, error)
DeleteBatch(context.Context, []string) error
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
ReplaceRuntimeReadOnly(context.Context, []model.DockerPortGuardPolicy) error
}
type DockerPortGuardRepo struct{}
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
func (r *DockerPortGuardRepo) List(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).
Where("read_only = ?", false).
Order("family, host_ip, host_port, protocol").
Find(&policies).Error
return policies, err
}
func (r *DockerPortGuardRepo) ListRuntimeReadOnly(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).
Where("read_only = ?", true).
Order("family, sequence, host_ip, host_port, protocol").
Find(&policies).Error
err := global.DB.WithContext(ctx).Order("family, host_ip, host_port, protocol").Find(&policies).Error
return policies, err
}
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
return global.DB.WithContext(ctx).
Where("read_only = ? AND uuid IN ?", false, uuids).
Delete(&model.DockerPortGuardPolicy{}).Error
return global.DB.WithContext(ctx).Where("uuid IN ?", uuids).Delete(&model.DockerPortGuardPolicy{}).Error
}
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for i := range policies {
policies[i].ReadOnly = false
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
Columns: []clause.Column{{Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
}).Create(&policies[i]).Error; err != nil {
return err
@@ -59,19 +42,3 @@ func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.
return nil
})
}
func (r *DockerPortGuardRepo) ReplaceRuntimeReadOnly(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("read_only = ?", true).
Delete(&model.DockerPortGuardPolicy{}).Error; err != nil {
return err
}
if len(policies) == 0 {
return nil
}
for i := range policies {
policies[i].ReadOnly = true
}
return tx.Create(&policies).Error
})
}
+6
View File
@@ -29,6 +29,12 @@ type FirewallRuleRepo struct {
db *gorm.DB
}
func WithFirewallRuleSource(kind, id string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("owner = ?", model.FirewallRuleOwner(kind, id))
}
}
func NewIFirewallRuleRepo() IFirewallRuleRepo {
return &FirewallRuleRepo{}
}
+2 -3
View File
@@ -936,7 +936,6 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
AccountID: agent.AccountID,
Model: model,
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
Metadata: extractOpenclawModelMetadata(conf, account, models),
}, nil
}
@@ -968,7 +967,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
if agent.AgentType != constant.AppOpenclaw {
return fmt.Errorf("%s does not support", agent.AgentType)
}
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
return err
}
}
@@ -1685,7 +1684,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return err
}
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
return err
}
case constant.AppHermesAgent:
+1 -18
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"path"
"slices"
"sort"
"strings"
"time"
@@ -1321,10 +1320,6 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
}
func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error {
help, err := cmd.RunDockerExecWithStdout(time.Minute, containerName, "openclaw", "plugins", "install", "--help")
if err != nil {
return err
}
workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
defer func() {
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
@@ -1346,19 +1341,7 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
if pkgPath == "" {
return fmt.Errorf("openclaw plugin package not found")
}
args := []string{"exec", containerName, "openclaw", "plugins", "install", pkgPath}
// Newer CLIs require source confirmation; older releases do not support --force.
options := strings.Fields(help)
if slices.Contains(options, "--force") {
args = append(args, "--force")
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
args = append(args, "--dangerously-force-unsafe-install")
}
// Source confirmation does not grant the selected channel plugin's capabilities.
if slices.Contains(options, "--accept-capabilities") {
args = append(args, "--accept-capabilities")
}
return mgr.Run("docker", args...)
return mgr.Run("docker", "exec", containerName, "openclaw", "plugins", "install", pkgPath, "--dangerously-force-unsafe-install")
}
func uninstallOpenclawPlugin(mgr *cmd.CommandHelper, containerName, pluginID string) error {
+6 -151
View File
@@ -10,7 +10,6 @@ import (
"net/url"
"path"
"regexp"
"slices"
"strconv"
"strings"
"time"
@@ -738,11 +737,9 @@ type modelProvider struct {
}
type modelEntry struct {
ID string `json:"id"`
Name string `json:"name"`
Input []string `json:"input,omitempty"`
ContextWindow int `json:"contextWindow,omitempty"`
MaxTokens int `json:"maxTokens,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Input []string `json:"input,omitempty"`
}
func requiresOpenclawProviderModels(provider string) bool {
@@ -770,7 +767,7 @@ type browserConfig struct {
DefaultProfile string `json:"defaultProfile"`
}
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
if strings.TrimSpace(confDir) == "" {
return fmt.Errorf("config dir is required")
}
@@ -855,7 +852,6 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
}
conf = initial
} else {
preserveOpenclawModelMetadata(conf, cfg.Models)
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
return err
}
@@ -910,9 +906,6 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
if allowedOrigins != nil {
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
}
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
return err
}
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err
}
@@ -927,144 +920,6 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
}
func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig {
raw, ok := conf["models"]
if !ok {
return nil
}
payload, err := json.Marshal(raw)
if err != nil {
return nil
}
var models modelsConfig
if err := json.Unmarshal(payload, &models); err != nil {
return nil
}
return &models
}
func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) {
current := readOpenclawModelsConfig(conf)
if current == nil || next == nil {
return
}
for providerID, nextProvider := range next.Providers {
currentProvider, ok := current.Providers[providerID]
if !ok {
continue
}
byID := make(map[string]modelEntry, len(currentProvider.Models))
for _, entry := range currentProvider.Models {
byID[entry.ID] = entry
}
for index := range nextProvider.Models {
currentEntry, ok := byID[nextProvider.Models[index].ID]
if !ok {
continue
}
nextProvider.Models[index].Input = currentEntry.Input
nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow
nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens
}
next.Providers[providerID] = nextProvider
}
}
func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata {
result := make([]dto.AgentModelMetadata, 0, len(accountModels))
configured := readOpenclawModelsConfig(conf)
for _, item := range accountModels {
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
if err != nil {
continue
}
metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"}
if configured != nil {
for _, entry := range configured.Providers[providerID].Models {
if entry.ID != inferred.ID {
continue
}
metadata.ContextWindow = entry.ContextWindow
metadata.MaxTokens = entry.MaxTokens
if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) {
if slices.Contains(entry.Input, "image") {
metadata.InputMode = "image"
} else {
metadata.InputMode = "text"
}
}
break
}
}
result = append(result, metadata)
}
return result
}
func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error {
if len(requested) == 0 {
return nil
}
configured := readOpenclawModelsConfig(conf)
if configured == nil {
return fmt.Errorf("model metadata is not supported for provider %s", account.Provider)
}
accountModels, err := loadAgentAccountModels(account)
if err != nil {
return err
}
available := make(map[string]dto.AgentAccountModel, len(accountModels))
for _, item := range accountModels {
available[item.ID] = item
}
seen := make(map[string]struct{}, len(requested))
for _, metadata := range requested {
item, ok := available[metadata.Model]
if !ok {
return buserr.New("ErrAgentModelNotInAccount")
}
if _, ok := seen[metadata.Model]; ok {
return fmt.Errorf("duplicate model metadata: %s", metadata.Model)
}
seen[metadata.Model] = struct{}{}
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
if err != nil {
return err
}
provider := configured.Providers[providerID]
found := false
for index := range provider.Models {
if provider.Models[index].ID != inferred.ID {
continue
}
found = true
provider.Models[index].ContextWindow = metadata.ContextWindow
provider.Models[index].MaxTokens = metadata.MaxTokens
switch metadata.InputMode {
case "auto":
provider.Models[index].Input = inferred.Input
case "text":
provider.Models[index].Input = []string{"text"}
case "image":
provider.Models[index].Input = []string{"text", "image"}
default:
return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode)
}
break
}
if !found {
return buserr.New("ErrAgentModelNotInAccount")
}
configured.Providers[providerID] = provider
}
modelsMap, err := structToMap(configured)
if err != nil {
return err
}
conf["models"] = modelsMap
return nil
}
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
accountModels, err := loadAgentAccountModels(account)
if err != nil {
@@ -1186,7 +1041,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
return fmt.Errorf("app install is required")
}
confDir := path.Join(appInstall.GetPath(), "data", "conf")
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
return err
}
dataDir := path.Join(appInstall.GetPath(), "data")
@@ -1483,7 +1338,7 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
func requiresInitialAgentAccountModels(provider string) bool {
switch provider {
case "custom", "vllm", "ollama", "llmman":
case "custom", "vllm", "ollama":
return true
default:
return false
+13 -2
View File
@@ -13,10 +13,14 @@ const (
vllmImageTypeNvidia = "nvidia"
vllmImageTypeIntel = "intel"
vllmImageTypeAscend = "ascend"
vllmImageTypeGB10 = "nvidia-gb10-dspark"
)
func resolveVllmVersionFamily(version, image string) string {
normalizedVersion := strings.ToLower(strings.TrimSpace(version))
if strings.HasPrefix(normalizedVersion, vllmImageTypeGB10+"-") {
return vllmImageTypeGB10
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
return vllmImageTypeIntel
}
@@ -27,6 +31,9 @@ func resolveVllmVersionFamily(version, image string) string {
return vllmImageTypeNvidia
}
normalizedImage := strings.ToLower(strings.TrimSpace(image))
if strings.Contains(normalizedImage, "vllm-gb10-dspark") {
return vllmImageTypeGB10
}
if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") {
return vllmImageTypeIntel
}
@@ -39,7 +46,7 @@ func resolveVllmVersionFamily(version, image string) string {
func trimVllmVersionFamily(version string) string {
trimmed := strings.TrimSpace(version)
normalized := strings.ToLower(trimmed)
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
for _, family := range []string{vllmImageTypeGB10, vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
prefix := family + "-"
if strings.HasPrefix(normalized, prefix) {
return strings.TrimSpace(trimmed[len(prefix):])
@@ -51,6 +58,9 @@ func trimVllmVersionFamily(version string) string {
func buildDefaultVllmImageByVersion(version string) string {
tag := trimVllmVersionFamily(version)
family := resolveVllmVersionFamily(version, "")
if family == vllmImageTypeGB10 {
return "1panel/vllm-gb10-dspark:" + tag
}
if family == vllmImageTypeIntel {
return "intel/llm-scaler-vllm:" + tag
}
@@ -71,7 +81,8 @@ func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage str
func hasVllmVersionFamilyPrefix(version string) bool {
normalized := strings.ToLower(strings.TrimSpace(version))
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
return strings.HasPrefix(normalized, vllmImageTypeGB10+"-") ||
strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
strings.HasPrefix(normalized, vllmImageTypeIntel+"-") ||
strings.HasPrefix(normalized, vllmImageTypeAscend+"-")
}
+27 -327
View File
@@ -17,13 +17,10 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/email"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
"github.com/shirou/gopsutil/v4/disk"
)
@@ -37,28 +34,6 @@ var communityAlertMethodTypeNames = map[string]string{
constant.SMS: "SMS",
}
var legacyAlertMethodTypeMap = map[string]string{
"mail": constant.Email,
constant.Email: constant.Email,
constant.SMS: constant.SMS,
constant.Bark: constant.Bark,
constant.WeChat: constant.WeCom,
constant.WeCom: constant.WeCom,
constant.DingTalk: constant.DingTalk,
constant.FeiShu: constant.FeiShu,
constant.Custom: constant.Custom,
}
var supportedAlertMethodTypes = map[string]struct{}{
constant.Email: {},
constant.SMS: {},
constant.Bark: {},
constant.WeCom: {},
constant.DingTalk: {},
constant.FeiShu: {},
constant.Custom: {},
}
type IAlertService interface {
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
GetAlerts() ([]dto.AlertDTO, error)
@@ -78,10 +53,8 @@ type IAlertService interface {
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
DeleteAlertConfig(id uint) error
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
}
func NewIAlertService() IAlertService {
@@ -207,15 +180,9 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
}
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
methodTypes, err := a.validateAlertMethodReferences(req.Method)
if err != nil {
if err := a.validateCommunityAlertMethod(req.Method); err != nil {
return err
}
if req.Status != constant.AlertDisable {
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
}
upMap := make(map[string]interface{})
upMap["id"] = req.ID
@@ -273,16 +240,7 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
if alertInfo.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
methodTypes, err := a.validateAlertMethodReferences(alertInfo.Method)
if err != nil {
return err
}
if status == constant.AlertEnable {
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
}
err = alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
err := alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
if err != nil {
return err
}
@@ -454,7 +412,6 @@ func (a AlertService) parseAlertLog(item model.AlertLog) (dto.AlertLogDTO, error
if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil {
return dto.AlertLogDTO{}, err
}
alertDetail.Task = nil
if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil {
return dto.AlertLogDTO{}, err
}
@@ -537,13 +494,7 @@ func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertCon
}
opts = append(opts, repo.WithByStatus(constant.AlertEnable))
configs, err := alertRepo.AlertConfigList(opts...)
if err != nil {
return nil, err
}
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
return nil, err
}
return configs, nil
return configs, err
}
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) {
@@ -554,49 +505,13 @@ func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []mode
if len(req.ExcludeTypes) > 0 {
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
}
total, configs, err := alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
if err != nil {
return 0, nil, err
}
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
return 0, nil, err
}
return total, configs, nil
return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
}
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
if req.Type == constant.Custom {
if req.ID != 0 && req.Revision == nil {
return repo.ErrAlertConfigRevisionRequired
}
return a.updateCustomAlertConfig(req, operator)
}
usesMutation, err := alertconfig.UsesMutation(req.Type, req.Config)
if err != nil {
return err
}
if req.ID != 0 && usesMutation && req.Revision == nil {
return repo.ErrAlertConfigRevisionRequired
}
var existing *model.AlertConfig
if req.ID != 0 {
stored, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return err
}
if stored.Type != req.Type {
return fmt.Errorf("alert config %d has type %s, not %s", req.ID, stored.Type, req.Type)
}
existing = &stored
}
if err := a.validateCommunityAlertConfigType(req.Type); err != nil {
return err
}
prepared, err := alertconfig.Prepare(req.Type, req.Config, req.Status, existing)
if err != nil {
return err
}
req.Config = prepared
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
return err
}
@@ -611,7 +526,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
upMap["status"] = req.Status
upMap["config"] = req.Config
upMap["update_user"] = operator
if err := alertRepo.UpdateAlertConfigWithRevision(upMap, req.Revision, repo.WithByID(req.ID)); err != nil {
if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil {
return err
}
} else {
@@ -629,99 +544,6 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
return nil
}
func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operator string) error {
if err := validateAlertConfigStatus(req.Status); err != nil {
return err
}
var existing *model.AlertConfig
if req.ID != 0 {
config, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return err
}
if config.Type != constant.Custom {
return fmt.Errorf("alert config %d is not a custom webhook", req.ID)
}
existing = &config
}
prepared, err := alertwebhook.Prepare(req.Config, req.Status, existing)
if err != nil {
return err
}
validatedReq := req
validatedReq.Config = prepared.Config
if err := a.checkAlertConfigDisplayNameUnique(validatedReq); err != nil {
return err
}
if existing != nil {
return alertRepo.UpdateAlertConfigWithRevision(map[string]interface{}{
"type": constant.Custom,
"title": req.Title,
"status": req.Status,
"config": prepared.Config,
"secret_config": prepared.SecretConfig,
"update_user": operator,
}, req.Revision, repo.WithByID(req.ID))
}
return alertRepo.CreateAlertConfig(&model.AlertConfig{
Type: constant.Custom,
Title: req.Title,
Status: req.Status,
Config: prepared.Config,
SecretConfig: prepared.SecretConfig,
CreateUser: operator,
UpdateUser: operator,
})
}
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
if err := validateAlertConfigStatus(req.Status); err != nil {
return err
}
config, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return err
}
if req.Status == constant.AlertEnable {
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
return err
}
if config.Type == constant.Custom {
if _, err := alertwebhook.Resolve(config); err != nil {
return err
}
}
}
return alertRepo.UpdateAlertConfig(map[string]interface{}{
"status": req.Status,
"update_user": operator,
}, repo.WithByID(req.ID))
}
func validateAlertConfigStatus(status string) error {
if status != constant.AlertEnable && status != constant.AlertDisable {
return fmt.Errorf("alert config status must be Enable or Disable")
}
return nil
}
func exposeCustomAlertConfigSecrets(configs []model.AlertConfig) error {
for index := range configs {
if configs[index].Type != constant.Custom {
continue
}
view, err := alertwebhook.PlainView(configs[index])
if err != nil {
return fmt.Errorf("build editable custom alert config %d: %w", configs[index].ID, err)
}
configs[index].Config = view
}
return nil
}
func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
if req.Type != constant.SMSConfig {
return nil
@@ -746,9 +568,6 @@ func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate)
}
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
if req.Type != constant.Custom && (global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn") {
return nil
}
displayName := alertConfigDisplayName(req.Type, req.Config)
if displayName == "" {
return nil
@@ -772,67 +591,37 @@ func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdat
}
func (a AlertService) validateCommunityAlertMethod(method string) error {
methodTypes, err := a.validateAlertMethodReferences(method)
if err != nil {
return err
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
return nil
}
return a.validateAlertMethodEntitlement(methodTypes)
}
func (a AlertService) validateAlertMethodReferences(method string) ([]string, error) {
if strings.TrimSpace(method) == "" {
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
return nil
}
methodTypes := make([]string, 0)
for _, item := range strings.Split(method, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
configType := ""
if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
config, err := alertRepo.GetConfigById(uint(configID))
if err != nil {
return nil, err
return err
}
configType = config.Type
} else {
var ok bool
configType, ok = legacyAlertMethodTypeMap[item]
if !ok {
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
if _, ok := communityAlertMethodTypeNames[config.Type]; ok {
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
}
}
if _, ok := supportedAlertMethodTypes[configType]; !ok {
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
}
methodTypes = append(methodTypes, configType)
}
if len(methodTypes) == 0 {
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
}
return methodTypes, nil
}
func (a AlertService) validateAlertMethodEntitlement(methodTypes []string) error {
for _, configType := range methodTypes {
if configType == constant.Custom {
continue
}
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
continue
}
if _, ok := communityAlertMethodTypeNames[configType]; ok {
if _, ok := communityAlertMethodTypeNames[item]; ok {
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
}
}
return nil
}
func (a AlertService) validateCommunityAlertConfigType(configType string) error {
if configType == constant.Custom {
return nil
}
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
return nil
}
@@ -844,7 +633,7 @@ func (a AlertService) validateCommunityAlertConfigType(configType string) error
func alertConfigDisplayName(configType, configData string) string {
switch configType {
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS, constant.Custom:
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS:
var cfg struct {
DisplayName string `json:"displayName"`
}
@@ -883,24 +672,20 @@ func (a AlertService) DeleteAlertConfig(id uint) error {
}
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
emailConfig, err := resolveEmailTestConfig(req)
if err != nil {
return false, err
}
username := emailConfig.UserName
username := req.UserName
if username == "" {
username = emailConfig.Sender
username = req.Sender
}
encodedDisplayName := mime.BEncoding.Encode("UTF-8", emailConfig.DisplayName)
encodedDisplayName := mime.BEncoding.Encode("UTF-8", req.DisplayName)
cfg := email.SMTPConfig{
Host: emailConfig.Host,
Port: emailConfig.Port,
Sender: emailConfig.Sender,
Host: req.Host,
Port: req.Port,
Sender: req.Sender,
Username: username,
Password: emailConfig.Password,
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, emailConfig.Sender),
Encryption: emailConfig.Encryption,
Recipient: emailConfig.Recipient,
Password: req.Password,
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender),
Encryption: req.Encryption,
Recipient: req.Recipient,
}
msg := email.EmailMessage{
@@ -915,94 +700,9 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
return true, nil
}
func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, error) {
emailConfig := dto.AlertEmailConfig{
Host: req.Host,
Port: req.Port,
Sender: req.Sender,
UserName: req.UserName,
Password: req.Password,
DisplayName: req.DisplayName,
Encryption: req.Encryption,
Recipient: req.Recipient,
}
if strings.TrimSpace(req.Config) != "" {
configType := req.Type
if configType == "" {
configType = constant.EmailConfig
}
if configType != constant.EmailConfig {
return dto.AlertEmailConfig{}, fmt.Errorf("alert config test type must be email")
}
var existing *model.AlertConfig
if req.ID != 0 {
stored, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return dto.AlertEmailConfig{}, err
}
existing = &stored
}
prepared, err := alertconfig.Prepare(configType, req.Config, constant.AlertEnable, existing)
if err != nil {
return dto.AlertEmailConfig{}, err
}
if err := json.Unmarshal([]byte(prepared), &emailConfig); err != nil {
return dto.AlertEmailConfig{}, fmt.Errorf("decode email alert config: %w", err)
}
}
return emailConfig, nil
}
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
if req.Type != constant.Custom {
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
}
var existing *model.AlertConfig
if req.ID != 0 {
config, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return dto.AlertConfigTestResult{}, err
}
if config.Type != constant.Custom {
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config %d is not a custom webhook", req.ID)
}
existing = &config
}
prepared, err := alertwebhook.Prepare(req.Config, constant.AlertEnable, existing)
if err != nil {
return dto.AlertConfigTestResult{}, err
}
resolved, err := alertwebhook.Resolve(model.AlertConfig{
Type: constant.Custom,
Config: prepared.Config,
SecretConfig: prepared.SecretConfig,
})
if err != nil {
return dto.AlertConfigTestResult{}, err
}
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
if !ok {
return dto.AlertConfigTestResult{
Success: false,
Message: providers.ErrCustomWebhookUnsupported.Error(),
}, nil
}
return tester.TestCustomWebhook(resolved)
}
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
var methodTypes []string
if updateAlert.SendCount != 0 || strings.TrimSpace(updateAlert.Method) != "" {
var err error
methodTypes, err = a.validateAlertMethodReferences(updateAlert.Method)
if err != nil {
return err
}
}
if updateAlert.SendCount != 0 {
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil {
return err
}
upMap := make(map[string]interface{})
var newStatus string
+28 -100
View File
@@ -2,7 +2,6 @@ package service
import (
"encoding/json"
"errors"
"fmt"
"math"
"net"
@@ -33,7 +32,6 @@ const (
ResourceAlertInterval = 30
CheckIntervalSec = 3
LoadCheckIntervalMin = 5
sshIPLoginWindow = 30 * time.Minute
)
type AlertTaskHelper struct {
@@ -514,28 +512,10 @@ func loadPanelLogin(alert dto.AlertDTO) {
}
func loadSSHLogin(alert dto.AlertDTO) {
now := time.Now()
failedWindow := time.Duration(alert.Cycle) * time.Minute
loadWindow := failedWindow
if loadWindow < sshIPLoginWindow {
loadWindow = sshIPLoginWindow
}
location, err := time.LoadLocation(common.LoadTimeZoneByCmd())
count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count)
if err != nil {
global.LOG.Errorf("Failed to load timezone for ssh login logs: %v", err)
location = time.Local
global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err)
}
histories, err := loadSSHAlertHistories(defaultSSHLogDir, now.Add(-loadWindow), now, location)
if err != nil {
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
}
count, records := summarizeSSHLoginHistories(
histories,
now,
failedWindow,
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
)
isAlert := count >= int(alert.Count)
if isAlert {
params := []dto.Param{
{
@@ -551,6 +531,12 @@ func loadSSHLogin(alert dto.AlertDTO) {
}
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params)
}
whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n")
records, err := alertUtil.FindRecentSuccessLoginNotInWhitelist(30, whitelist)
if err != nil {
global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err)
}
records = filterSSHLoginEntriesNotInWhitelist(records, whitelist)
if len(records) > 0 {
quota := strings.Join(records, "\n")
params := []dto.Param{
@@ -579,6 +565,20 @@ func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string)
return filtered
}
func filterSSHLoginEntriesNotInWhitelist(records []string, whitelist []string) []string {
filtered := make([]string, 0, len(records))
for _, record := range records {
ip := record
if idx := strings.Index(record, "-"); idx >= 0 {
ip = record[:idx]
}
if !isIPInWhitelist(ip, whitelist) {
filtered = append(filtered, record)
}
}
return filtered
}
func isIPInWhitelist(ip string, whitelist []string) bool {
targetIP := net.ParseIP(strings.TrimSpace(ip))
if targetIP == nil {
@@ -695,10 +695,9 @@ func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
typeMap := map[string]string{
"mail": constant.Email,
constant.Bark: constant.Bark,
constant.SMS: constant.SMS,
constant.Custom: constant.Custom,
"mail": constant.Email,
constant.Bark: constant.Bark,
constant.SMS: constant.SMS,
}
configType, ok := typeMap[method]
if !ok {
@@ -786,7 +785,7 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
case constant.WeCom, constant.DingTalk, constant.FeiShu:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid {
return
@@ -799,31 +798,12 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false
var alertErr error
if config.Type == constant.Custom {
task := dto.AlertTaskMetadata{
AlertID: alert.ID,
Type: alertType,
Quota: quota,
QuotaType: quotaType,
Method: methodStr,
}
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo, task)
queued, alertErr = result.Queued, deliveryErr
if alertErr == nil && result.Queued {
_, alertErr = alertUtil.RecordQueuedAlertTask(result.LogID, task)
}
} else {
alertErr = xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
}
alertErr := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
if alertErr != nil {
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
return
}
if !queued {
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
}
}
@@ -1117,55 +1097,3 @@ func calculateMinutesDifference(newDate time.Time) int {
minutesDifference := int(now.Sub(newDate).Minutes())
return minutesDifference
}
func loadSSHAlertHistories(
baseDir string,
startTime, endTime time.Time,
location *time.Location,
) ([]dto.SSHHistory, error) {
fileList, err := listSSHLogFiles(baseDir)
if err != nil {
return nil, err
}
var (
histories []dto.SSHHistory
loadErr error
)
for _, file := range fileList {
items, err := loadSSHHistoriesFromFile(file.Name, "", "", startTime, endTime, file.Year, location)
if err != nil {
loadErr = errors.Join(loadErr, fmt.Errorf("load SSH log file %s: %w", file.Name, err))
continue
}
histories = append(histories, items...)
}
return histories, loadErr
}
func summarizeSSHLoginHistories(
histories []dto.SSHHistory,
now time.Time,
failedWindow time.Duration,
whitelist []string,
) (int, []string) {
failedStartTime := now.Add(-failedWindow)
successStartTime := now.Add(-sshIPLoginWindow)
failedCount := 0
var abnormalLogins []string
for _, item := range histories {
switch item.Status {
case constant.StatusFailed:
if isSSHLogWithinTimeRange(item.Date, failedStartTime, now) {
failedCount++
}
case constant.StatusSuccess:
if !isSSHLogWithinTimeRange(item.Date, successStartTime, now) || isIPInWhitelist(item.Address, whitelist) {
continue
}
abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", item.Address, item.Date.Format(constant.DateTimeLayout)))
}
}
return failedCount, abnormalLogins
}
+6 -45
View File
@@ -75,7 +75,7 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
} else {
s.sendBarkWithConfig(config, quota, params)
}
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
case constant.WeCom, constant.DingTalk, constant.FeiShu:
if isResource {
s.sendResourceWebhookWithConfig(config, quota, params)
} else {
@@ -86,7 +86,7 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
alertRepo := repo.NewIAlertRepo()
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS, constant.Custom: constant.Custom}
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS}
configType := method
if mapped, ok := typeMap[method]; ok {
configType = mapped
@@ -308,31 +308,12 @@ func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota stri
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false
var err error
if config.Type == constant.Custom {
task := dto.AlertTaskMetadata{
AlertID: s.alert.ID,
Type: s.alert.Type,
Quota: quota,
QuotaType: s.quotaType,
Method: strconv.Itoa(int(config.ID)),
}
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
queued, err = result.Queued, deliveryErr
if err == nil && result.Queued {
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
}
} else {
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
}
err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return
}
if !queued {
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
@@ -353,31 +334,11 @@ func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, qu
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false
var err error
if config.Type == constant.Custom {
task := dto.AlertTaskMetadata{
AlertID: s.alert.ID,
Type: s.alert.Type,
Quota: quota,
QuotaType: s.quotaType,
Method: strconv.Itoa(int(config.ID)),
}
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
queued, err = result.Queued, deliveryErr
if err == nil && result.Queued {
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
}
} else {
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
}
if err != nil {
if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo); err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return
}
if !queued {
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
+8 -13
View File
@@ -223,9 +223,6 @@ func (a AppService) GetAppDetailByKey(appKey, version string) (response.AppDetai
if err != nil {
return appDetailDTO, err
}
if err = checkVllmVersionAccess(app.Key, version); err != nil {
return appDetailDTO, err
}
appDetail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(version))
if err != nil {
return appDetailDTO, err
@@ -244,17 +241,14 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
if err != nil {
return appDetailDTO, err
}
app, err := appRepo.GetFirst(repo.WithByID(detail.AppId))
if err != nil {
return appDetailDTO, err
}
if err = checkVllmVersionAccess(app.Key, detail.Version); err != nil {
return appDetailDTO, err
}
appDetailDTO.AppDetail = detail
appDetailDTO.Enable = true
if appType == "runtime" {
app, err := appRepo.GetFirst(repo.WithByID(appID))
if err != nil {
return appDetailDTO, err
}
fileOp := files.NewFileOp()
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
@@ -325,6 +319,10 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose)
app, err := appRepo.GetFirst(repo.WithByID(detail.AppId))
if err != nil {
return appDetailDTO, err
}
if err := checkLimit(app); err != nil {
appDetailDTO.Enable = false
}
@@ -376,9 +374,6 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
if err != nil {
return
}
if err = checkVllmVersionAccess(app.Key, appDetail.Version); err != nil {
return
}
if DatabaseKeys[app.Key] > 0 {
if existDatabases, _ := databaseRepo.GetList(repo.WithByName(req.Name)); len(existDatabases) > 0 {
err = buserr.New("ErrRemoteExist")
-3
View File
@@ -675,9 +675,6 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
return versions, err
}
for _, detail := range details {
if !canAccessVllmVersion(app.Key, detail.Version) {
continue
}
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
if len(ignores) > 0 {
continue
-24
View File
@@ -107,9 +107,6 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
return err
}
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
return errors.New("vLLM can only upgrade within the same image type")
}
@@ -462,13 +459,6 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
}); err != nil {
return err
}
// Upgrades deliberately keep the user's nginx.conf, so corrected gzip
// defaults shipped with a new version would never reach existing
// installations. Rewrite only an untouched factory configuration, and
// never fail the upgrade over it.
if gzipErr := upgradeStockNginxGzipConfig(u.candidate); gzipErr != nil {
t.Logf("WARNING: update stock gzip configuration failed, keeping the current one: %v", gzipErr)
}
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
return err
}
@@ -725,20 +715,6 @@ func renderUpgradeEnv(install *model.AppInstall, original []byte) ([]byte, error
return nil, err
}
handleMap(envs, params)
if install.App.Key == "openlist" {
// The upgrade script updates this too late for the pre-pull phase.
image := "openlistteam/openlist:v" + strings.TrimPrefix(install.Version, "v")
if preInstalled := params["PRE_INSTALLED"]; preInstalled != "" {
image += "-" + preInstalled
}
params["OPENLIST_IMAGE"] = image
envs["OPENLIST_IMAGE"] = image
content, err := json.Marshal(envs)
if err != nil {
return nil, err
}
install.Env = string(content)
}
if install.App.Key == constant.AppOpenresty {
for _, key := range []string{"CONTAINER_PACKAGE_URL", "RESTY_ADD_PACKAGE_BUILDDEPS", "RESTY_CONFIG_OPTIONS_MORE"} {
if value, ok := originalEnv[key]; ok {
-3
View File
@@ -2249,9 +2249,6 @@ func getAppVersions(key string, details []model.AppDetail) []string {
hasLatest := false
latestVersion := ""
for _, detail := range details {
if !canAccessVllmVersion(key, detail.Version) {
continue
}
if key != "mssql" && strings.Contains(detail.Version, "latest") {
hasLatest = true
latestVersion = detail.Version
+54 -28
View File
@@ -582,10 +582,6 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
if config.Image == "" {
return fmt.Errorf("container image not found in backup file")
}
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(recoverCtx.inspectInfo.NetworkSettings, hostConfig)
if err := normalizeContainerEndpointSettings(ctx, recoverCtx.client, networkConf, extraNetworks); err != nil {
return err
}
if !checkImageExist(recoverCtx.client, config.Image) {
if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil {
return err
@@ -600,7 +596,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return err
}
createRes, err := createContainerWithNetworks(ctx, recoverCtx.client, config, hostConfig, networkConf, extraNetworks, recoverCtx.targetName)
createRes, err := createContainerWithOldNetworks(ctx, recoverCtx.client, config, hostConfig, recoverCtx.inspectInfo.NetworkSettings, recoverCtx.targetName)
if err != nil {
return err
}
@@ -608,7 +604,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return nil
}
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) error {
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
removeEndpointMacAddresses(primary, extras)
}
@@ -623,14 +619,11 @@ func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client,
}
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
if err != nil {
return fmt.Errorf("inspect network %s failed: %w", netName, err)
}
if err := validateContainerEndpointStaticIP(netName, info, endpoint); err != nil {
return err
continue
}
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
}
}
return nil
}
func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
@@ -648,28 +641,24 @@ func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[st
}
}
func validateContainerEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) error {
func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) {
if endpoint == nil || endpoint.IPAMConfig == nil {
return nil
return
}
ipam := endpoint.IPAMConfig
if err := ipam.Validate(); err != nil {
return fmt.Errorf("invalid IP configuration for network %s: %w", netName, err)
}
if ipam.IPv4Address == "" && ipam.IPv6Address == "" {
return nil
}
if netName == "host" || netName == "none" || isDefaultBridgeNetwork(netName, info) {
return fmt.Errorf("network %s does not support static IP configuration", netName)
if isDefaultBridgeNetwork(netName, info) {
endpoint.IPAMConfig = nil
return
}
if ipam.IPv4Address != "" && !networkSupportsStaticIP(info, ipam.IPv4Address, false) {
return fmt.Errorf("static IPv4 address %s is not in a configured subnet of network %s", ipam.IPv4Address, netName)
if endpoint.IPAMConfig.IPv4Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv4Address, false) {
endpoint.IPAMConfig.IPv4Address = ""
}
if ipam.IPv6Address != "" && !networkSupportsStaticIP(info, ipam.IPv6Address, true) {
return fmt.Errorf("static IPv6 address %s is not in a configured subnet of network %s", ipam.IPv6Address, netName)
if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
endpoint.IPAMConfig.IPv6Address = ""
}
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 {
endpoint.IPAMConfig = nil
}
return nil
}
func isDefaultBridgeNetwork(netName string, info network.Inspect) bool {
@@ -684,7 +673,6 @@ func networkSupportsStaticIP(info network.Inspect, ip string, isIPv6 bool) bool
if err != nil {
return false
}
addr = addr.Unmap()
if addr.Is6() != isIPv6 {
return false
}
@@ -825,6 +813,11 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
IPv6Address: endpoint.IPAMConfig.IPv6Address,
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...),
}
} else if name != "bridge" && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "") {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAddress,
IPv6Address: endpoint.GlobalIPv6Address,
}
}
if name == primaryName {
config.EndpointsConfig[name] = endpointSetting
@@ -838,6 +831,39 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
return config, extraNetworks
}
const unsupportedUserSpecifiedIPAddress = "user specified IP address is supported only when connecting to networks with user configured subnets"
func clearUnsupportedDynamicEndpointIPAM(err error, endpoints map[string]*network.EndpointSettings, networkSettings *container.NetworkSettings) bool {
if err == nil || !strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
return false
}
for name, endpoint := range endpoints {
if !isDynamicContainerNetwork(networkSettings, name) || endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
if strings.Contains(err.Error(), "network "+name+":") {
endpoint.IPAMConfig = nil
return true
}
}
cleared := false
for name, endpoint := range endpoints {
if isDynamicContainerNetwork(networkSettings, name) && endpoint != nil && endpoint.IPAMConfig != nil {
endpoint.IPAMConfig = nil
cleared = true
}
}
return cleared
}
func isDynamicContainerNetwork(networkSettings *container.NetworkSettings, name string) bool {
if networkSettings == nil || name == "bridge" {
return false
}
endpoint := networkSettings.Networks[name]
return endpoint != nil && endpoint.IPAMConfig == nil && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "")
}
func cloneContainerConfig(config *container.Config) *container.Config {
if config == nil {
return &container.Config{}
+47 -59
View File
@@ -16,7 +16,6 @@ import (
"path"
"path/filepath"
"regexp"
"slices"
"sort"
"strconv"
"strings"
@@ -25,7 +24,6 @@ import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
@@ -81,7 +79,7 @@ type IContainerService interface {
ContainerUpgrade(req dto.ContainerUpgrade) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
ContainerListStats() ([]dto.ContainerListStats, error)
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error)
LoadResourceLimit() (*dto.ResourceLimit, error)
ContainerRename(req dto.ContainerRename) error
ContainerCommit(req dto.ContainerCommit) error
@@ -248,15 +246,15 @@ func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
}
return data, nil
}
func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error) {
func (u *ContainerService) ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error) {
var data dto.ContainerItemStats
client, err := docker.NewDockerClient()
if err != nil {
return data, err
}
defer client.Close()
if req.Name != "system" {
containerInfo, _, err := client.ContainerInspectWithRaw(ctx, req.Name, true)
defer client.Close()
containerInfo, _, err := client.ContainerInspectWithRaw(context.Background(), req.Name, true)
if err != nil {
return data, err
}
@@ -265,7 +263,7 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
return data, nil
}
usage, err := client.DiskUsage(ctx, types.DiskUsageOptions{})
usage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{})
if err != nil {
return data, err
}
@@ -536,9 +534,7 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if err != nil {
return err
}
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil {
return err
}
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
@@ -648,9 +644,14 @@ func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) d
if endpoint.IPAMConfig != nil {
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
}
if name != "bridge" && endpoint.IPAMConfig != nil {
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
if name != "bridge" {
if endpoint.IPAMConfig != nil {
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
} else {
item.Ipv4 = endpoint.IPAddress
item.Ipv6 = endpoint.GlobalIPv6Address
}
}
return item
}
@@ -1677,44 +1678,32 @@ func checkImageLike(client *client.Client, imageName string) bool {
func pullImages(task *task.Task, client *client.Client, imageName string) error {
dockerCli := docker.NewClientWithExist(client)
repos, err := imageRepoRepo.List()
if err != nil {
return err
}
imageRepo := selectImageRepo(imageName, repos)
if imageRepo == nil || !imageRepo.Auth {
return dockerCli.PullImageWithProcess(task, imageName)
}
options := image.PullOptions{}
authConfig := registry.AuthConfig{
Username: imageRepo.Username,
Password: imageRepo.Password,
repos, _ := imageRepoRepo.List()
if len(repos) != 0 {
for _, repo := range repos {
if strings.HasPrefix(imageName, repo.DownloadUrl) && repo.Auth {
authConfig := registry.AuthConfig{
Username: repo.Username,
Password: repo.Password,
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return err
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
options.RegistryAuth = authStr
}
}
} else {
hasAuth, authStr := loadAuthInfo(imageName)
if hasAuth {
options.RegistryAuth = authStr
}
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return err
}
options.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
return dockerCli.PullImageWithProcessAndOptions(task, imageName, options)
}
func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo {
var selected *model.ImageRepo
selectedURLLength := 0
for i := range repos {
downloadURL := strings.TrimRight(strings.TrimSpace(repos[i].DownloadUrl), "/")
if downloadURL == "" || !strings.HasPrefix(imageName, downloadURL+"/") {
continue
}
if len(downloadURL) > selectedURLLength {
selected = &repos[i]
selectedURLLength = len(downloadURL)
}
}
return selected
}
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
data := dto.ContainerListStats{
ContainerID: containerItem,
@@ -1769,10 +1758,7 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
}
for i := 0; i <= hostEnd-hostStart; i++ {
bindItem := nat.PortBinding{HostPort: strconv.Itoa(hostStart + i), HostIP: port.HostIP}
portKey := nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))
if !slices.Contains(portMap[portKey], bindItem) {
portMap[portKey] = append(portMap[portKey], bindItem)
}
portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
}
for i := hostStart; i <= hostEnd; i++ {
if checkInUse && common.ScanPortWithIP(port.HostIP, i) {
@@ -1790,10 +1776,7 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
}
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
portKey := nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))
if !slices.Contains(portMap[portKey], bindItem) {
portMap[portKey] = append(portMap[portKey], bindItem)
}
portMap[nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))] = []nat.PortBinding{bindItem}
}
}
return portMap, nil
@@ -1972,7 +1955,7 @@ func loadComposeCount(client *client.Client) int {
}
func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
var exposedPorts []dto.PortHelper
seenPorts := make(map[dto.PortHelper]struct{})
samePortMap := make(map[string]dto.PortHelper)
ports := transPortToStr(itemPorts)
for _, item := range ports {
itemStr := strings.Split(item, "->")
@@ -1993,11 +1976,16 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
}
itemPort.ContainerPort = itemContainer[0]
itemPort.Protocol = itemContainer[1]
if _, exists := seenPorts[itemPort]; exists {
continue
keyItem := fmt.Sprintf("%s->%s/%s", itemPort.HostPort, itemPort.ContainerPort, itemPort.Protocol)
if val, ok := samePortMap[keyItem]; ok {
val.HostIP = ""
samePortMap[keyItem] = val
} else {
samePortMap[keyItem] = itemPort
}
seenPorts[itemPort] = struct{}{}
exposedPorts = append(exposedPorts, itemPort)
}
for _, val := range samePortMap {
exposedPorts = append(exposedPorts, val)
}
return exposedPorts
}
+45 -20
View File
@@ -67,12 +67,12 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
if err != nil {
return err
}
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil {
return err
}
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
cleanupErr, err := switchContainer(ctx, client, req.Name, oldContainer, func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
return createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
}, networkConf.EndpointsConfig, oldContainer.NetworkSettings)
}, config.Tty, t)
if err != nil {
return fmt.Errorf("update container failed, err: %v", err)
@@ -138,14 +138,8 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
config.Image = req.Image
hostConf := cloneContainerHostConfig(oldContainer.HostConfig)
preserveContainerVolumeMounts(hostConf, oldContainer.Mounts)
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, hostConf)
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks); err != nil {
upgradeErr := fmt.Errorf("prepare networks for container %s failed: %w", item, err)
upgradeErrors = append(upgradeErrors, upgradeErr)
return upgradeErr
}
cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) {
return createContainerWithNetworks(ctx, client, config, hostConf, networkConf, extraNetworks, item)
return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
}, config.Tty, t)
if err != nil {
upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err)
@@ -248,8 +242,9 @@ func (l *containerOperationMutex) lock(names ...string) func() {
}
type containerNetworkAttachment struct {
name string
endpoint *network.EndpointSettings
name string
endpoint *network.EndpointSettings
isDynamic bool
}
type containerSwitchLogger interface {
@@ -587,13 +582,13 @@ func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitc
endpoints := make(map[string]*network.EndpointSettings, len(extras)+1)
if primary != nil {
for name, endpoint := range primary.EndpointsConfig {
if name != "bridge" && endpoint != nil {
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint
}
}
}
for name, endpoint := range extras {
if name != "bridge" && endpoint != nil {
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint
}
}
@@ -609,8 +604,9 @@ func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitc
return disconnected, fmt.Errorf("disconnect original container from network %s failed: %w", name, err)
}
disconnected = append(disconnected, containerNetworkAttachment{
name: name,
endpoint: endpoints[name],
name: name,
endpoint: endpoints[name],
isDynamic: isDynamicContainerNetwork(oldContainer.NetworkSettings, name),
})
}
return disconnected, nil
@@ -620,6 +616,10 @@ func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitch
var reconnectErr error
for _, attachment := range attachments {
err := cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
if err != nil && attachment.isDynamic && strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
attachment.endpoint.IPAMConfig = nil
err = cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
}
if err != nil {
reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect original container to network %s failed: %w", attachment.name, err))
}
@@ -652,7 +652,7 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks)
logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr)
rollbackErr = errors.Join(rollbackErr, reconnectErr)
if wasRunning && reconnectErr == nil {
if wasRunning {
restartErr := restartOriginalContainer(ctx, cli, oldContainerID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr)
rollbackErr = errors.Join(rollbackErr, restartErr)
@@ -660,8 +660,17 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
return rollbackErr
}
func createContainerWithNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *network.NetworkingConfig, extraNetworks map[string]*network.EndpointSettings, name string) (container.CreateResponse, error) {
created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks)
var primaryEndpoints map[string]*network.EndpointSettings
if networkConf != nil {
primaryEndpoints = networkConf.EndpointsConfig
}
created, err := createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
}, primaryEndpoints, networkSettings)
if err != nil {
return created, err
}
@@ -673,6 +682,9 @@ func createContainerWithNetworks(ctx context.Context, client *client.Client, con
sort.Strings(extraNames)
for _, item := range extraNames {
err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
if clearUnsupportedDynamicEndpointIPAM(err, map[string]*network.EndpointSettings{item: extraNetworks[item]}, networkSettings) {
err = client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
}
if err != nil {
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
return created, err
@@ -680,3 +692,16 @@ func createContainerWithNetworks(ctx context.Context, client *client.Client, con
}
return created, nil
}
func createContainerWithDynamicIPFallback(
create func() (container.CreateResponse, error),
endpoints map[string]*network.EndpointSettings,
networkSettings *container.NetworkSettings,
) (container.CreateResponse, error) {
for {
created, err := create()
if err == nil || created.ID != "" || !clearUnsupportedDynamicEndpointIPAM(err, endpoints, networkSettings) {
return created, err
}
}
}
+1 -118
View File
@@ -2,7 +2,6 @@ package service
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
@@ -15,19 +14,14 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
)
const dockerNftablesMinVersion = "29.0.0"
type DockerService struct{}
type IDockerService interface {
UpdateConf(req dto.SettingUpdate, withRestart bool) error
UpdateFirewallBackend(backend string) error
UpdateLogOption(req dto.LogOption) error
UpdateIpv6Option(req dto.Ipv6Option) error
UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error
@@ -36,115 +30,6 @@ type IDockerService interface {
OperateDocker(req dto.DockerOperation) error
}
func loadDockerEngineVersion(ctx context.Context) string {
client, err := docker.NewDockerClient()
if err == nil {
defer client.Close()
if version, versionErr := client.ServerVersion(ctx); versionErr == nil && version.Version != "" {
return version.Version
}
}
if !cmd.Which("dockerd") {
return ""
}
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("dockerd", "--version")
if err != nil {
return ""
}
return strings.TrimSpace(stdout)
}
func dockerNftablesSupported(version string) bool {
return version != "" && common.CompareAppVersion(version, dockerNftablesMinVersion)
}
func applyDockerFirewallBackendConfig(daemonMap map[string]interface{}, backend, version string) error {
switch backend {
case constant.FirewallProviderNftables:
if !dockerNftablesSupported(version) {
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
}
daemonMap["experimental"] = true
daemonMap["firewall-backend"] = constant.FirewallProviderNftables
case constant.FirewallProviderIptables:
if dockerNftablesSupported(version) {
daemonMap["firewall-backend"] = constant.FirewallProviderIptables
} else {
delete(daemonMap, "firewall-backend")
}
default:
return fmt.Errorf("unsupported Docker firewall backend %q", backend)
}
return nil
}
func (u *DockerService) UpdateFirewallBackend(backend string) error {
version := loadDockerEngineVersion(context.Background())
if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) {
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
}
if backend == constant.FirewallProviderNftables {
if err := docker_guard.CheckIPv4Forwarding(); err != nil {
return err
}
}
original, readErr := os.ReadFile(constant.DaemonJsonPath)
existed := readErr == nil
if readErr != nil && !os.IsNotExist(readErr) {
return readErr
}
daemonMap := make(map[string]interface{})
if len(bytes.TrimSpace(original)) > 0 {
if err := json.Unmarshal(original, &daemonMap); err != nil {
return fmt.Errorf("failed to parse Docker configuration: %w", err)
}
}
if err := applyDockerFirewallBackendConfig(daemonMap, backend, version); err != nil {
return err
}
updated, err := json.MarshalIndent(daemonMap, "", "\t")
if err != nil {
return err
}
if existed && bytes.Equal(bytes.TrimSpace(original), bytes.TrimSpace(updated)) {
return nil
}
if err := os.MkdirAll(path.Dir(constant.DaemonJsonPath), 0755); err != nil {
return err
}
if err := os.WriteFile(constant.DaemonJsonPath, updated, 0640); err != nil {
return err
}
restore := func() error {
if existed {
return os.WriteFile(constant.DaemonJsonPath, original, 0640)
}
err := os.Remove(constant.DaemonJsonPath)
if os.IsNotExist(err) {
return nil
}
return err
}
if err := validateDockerConfig(); err != nil {
if restoreErr := restore(); restoreErr != nil {
return fmt.Errorf("%v; failed to restore Docker configuration: %w", err, restoreErr)
}
return err
}
if err := controller.HandleRestart("docker"); err != nil {
cause := fmt.Errorf("failed to restart Docker: %w", err)
if restoreErr := restore(); restoreErr != nil {
return fmt.Errorf("%v; failed to restore Docker configuration: %w", cause, restoreErr)
}
if restoreRestartErr := controller.HandleRestart("docker"); restoreRestartErr != nil {
return fmt.Errorf("%v; the previous configuration was restored but Docker could not be restarted: %w", cause, restoreRestartErr)
}
return cause
}
return nil
}
func NewIDockerService() IDockerService {
return &DockerService{}
}
@@ -282,9 +167,7 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro
delete(daemonMap, "ipv6")
delete(daemonMap, "fixed-cidr-v6")
delete(daemonMap, "ip6tables")
if configuredDockerFirewallBackend() != constant.FirewallProviderNftables {
delete(daemonMap, "experimental")
}
delete(daemonMap, "experimental")
}
case "LogOption":
if req.Value == "disable" {
-5
View File
@@ -159,10 +159,6 @@ func (f *FileService) SearchUploadWithPage(req request.SearchUploadWithPage) (in
})
}
sort.SliceStable(files, func(i, j int) bool {
return files[i].CreatedAt > files[j].CreatedAt
})
total, start, end := len(files), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
backData = make([]response.UploadInfo, 0)
@@ -896,7 +892,6 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
key := "file-wget-" + common.GetUuid()
options := files.DownloadOptions{
IgnoreCertificate: w.IgnoreCertificate,
UseServerFilename: w.UseServerFilename,
}
if w.UseProxy {
systemProxy, err := NewISettingService().GetSystemProxy()
+1304 -1819
View File
File diff suppressed because it is too large Load Diff
+130 -556
View File
@@ -6,25 +6,19 @@ import (
"errors"
"fmt"
"net/netip"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
agenti18n "github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/system"
@@ -34,36 +28,10 @@ import (
)
const (
dockerGuardComposeProjectLabel = "com.docker.compose.project"
dockerGuardComposeCreatedBy = "createdBy"
dockerTrafficPathForward = "forward"
dockerTrafficPathInput = "input"
dockerTrafficPathUnknown = "unknown"
dockerManagementContainerGuard = "container_guard"
dockerManagementHostFirewall = "host_firewall"
dockerManagementNeedsDiagnosis = "needs_diagnosis"
dockerReasonNATInspectFailed = "nat_inspect_failed"
dockerReasonNATChainUnreachable = "nat_chain_unreachable"
dockerReasonProxyInspectFailed = "proxy_inspect_failed"
dockerReasonNoMatchingPath = "no_matching_path"
dockerGuardComposeProjectLabel = "com.docker.compose.project"
dockerGuardComposeCreatedBy = "createdBy"
)
type dockerProxyEndpoint struct {
protocol string
hostIP string
hostPort uint16
}
type dockerForwardRules struct {
output string
inspected bool
}
type dockerProxyEndpoints struct {
items []dockerProxyEndpoint
inspected bool
}
type dockerGuardRuntime = docker_guard.Runtime
type DockerPortGuardService struct {
@@ -75,22 +43,18 @@ type DockerPortGuardService struct {
}
var (
dockerPortGuardServiceMu sync.Mutex
dockerPortGuardSyncMu sync.RWMutex
dockerPortGuardSyncErr error
ErrDockerGuardInvalid = docker_guard.ErrInvalidPolicy
ErrDockerUnavailable = docker.ErrUnavailable
ErrDockerIptablesChainUnavailable = docker_guard.ErrDockerIptablesChainUnavailable
ErrDockerNftablesChainUnavailable = docker_guard.ErrDockerNftablesChainUnavailable
dockerPortGuardServiceMu sync.Mutex
dockerPortGuardSyncMu sync.RWMutex
dockerPortGuardSyncErr error
ErrDockerGuardInvalid = docker_guard.ErrInvalidPolicy
ErrDockerUnavailable = docker.ErrUnavailable
)
type IDockerPortGuardService interface {
LoadOverview(context.Context) (dto.DockerPortGuardList, error)
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
Operate(context.Context, dto.DockerPortGuardOperation) error
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
DeletePolicies(context.Context, dto.DockerPortGuardPolicyBatchDelete) error
UpsertPolicies(context.Context, dto.DockerPortGuardPolicyBatch) error
Reconcile(context.Context) error
}
@@ -119,64 +83,36 @@ func ReconcileDockerPortGuardBestEffort(ctx context.Context) {
}
}
func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
cli, err := s.client()
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
}
defer cli.Close()
if socketPath, local := strings.CutPrefix(cli.DaemonHost(), "unix://"); local {
if _, statErr := os.Stat(socketPath); errors.Is(statErr, os.ErrNotExist) {
return []dto.DockerPortGuardContainer{}, nil
}
}
endpoints, err := discoverDockerEndpoints(ctx, cli, false)
if err != nil {
return nil, err
}
backend := selectedDockerFirewallBackend("")
if info, infoErr := cli.Info(ctx); infoErr == nil {
backend = dockerFirewallBackend(info)
}
annotateDockerEndpointManagement(endpoints, backend)
return groupDockerGuardContainers(endpoints), nil
}
func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
policies, err := s.policies.ListManaged(ctx)
selectedBackend := selectedDockerFirewallBackend("")
base := s.runtimeStatus(s.guardRuntime(selectedBackend), selectedBackend)
base.Version = s.loadFirewallVersion(selectedBackend)
policies, err := s.policies.List(ctx)
if err != nil {
return dto.DockerPortGuardList{}, err
}
unavailable := func() dto.DockerPortGuardList {
backend := selectedDockerFirewallBackend("")
base := s.runtimeStatus(s.guardRuntime(backend), backend)
base.Version = s.loadFirewallVersion(backend)
base.Message = agenti18n.Get("ErrDockerFailed")
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)}
}
cli, err := s.client()
if err != nil {
return unavailable(), nil
base.Message = agenti18n.Get("ErrDockerFailed")
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)}, nil
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return unavailable(), nil
base.Message = agenti18n.Get("ErrDockerFailed")
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)}, nil
}
detectedBackend := dockerFirewallBackend(info)
backend := selectedDockerFirewallBackend(detectedBackend)
base := s.runtimeStatus(s.guardRuntime(backend), backend)
base.Version = s.loadFirewallVersion(backend)
base.Backend = selectedDockerFirewallBackend(dockerFirewallBackend(info))
base = s.runtimeStatus(s.guardRuntime(base.Backend), base.Backend)
base.Version = s.loadFirewallVersion(base.Backend)
if reconcileErr := lastDockerPortGuardReconcileError(); reconcileErr != nil {
base.Message = reconcileErr.Error()
markDockerGuardReconcileFailure(&base, reconcileErr)
}
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
endpoints, err := discoverDockerEndpoints(ctx, cli)
if err != nil {
return dto.DockerPortGuardList{}, err
}
annotateDockerEndpointManagement(endpoints, detectedBackend)
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
sort.Slice(endpoints, func(i, j int) bool {
return guardEndpointKey(endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < guardEndpointKey(endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
@@ -204,8 +140,7 @@ func matchDockerGuardPolicies(
}
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.UUID, policy.Mode, docker_guard.DecodeSources(policy.Sources)
endpoints[i].Description = policy.Description
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard &&
((policy.Family == docker_guard.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == docker_guard.FamilyIPv6 && base.IPv6.Effective))
endpoints[i].Effective = (policy.Family == docker_guard.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == docker_guard.FamilyIPv6 && base.IPv6.Effective)
delete(byEndpoint, key)
}
orphanPolicies := make([]dto.DockerPortGuardEndpoint, 0, len(byEndpoint))
@@ -213,26 +148,11 @@ func matchDockerGuardPolicies(
orphanPolicies = append(orphanPolicies, dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description,
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
})
}
return endpoints, orphanPolicies
}
func initializeDockerGuardRuntime(runtime dockerGuardRuntime, policies []docker_guard.Policy) error {
err := runtime.Initialize(policies)
if errors.Is(err, docker_guard.ErrDockerForwardPolicyDrop) {
family := "IPv4"
var familyErr *docker_guard.FamilyError
if errors.As(err, &familyErr) && familyErr.Family == docker_guard.FamilyIPv6 {
family = "IPv6"
}
return buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": family}, err)
}
return err
}
func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.DockerPortGuardOperation) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
@@ -246,7 +166,7 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
if err != nil {
return err
}
if err := initializeDockerGuardRuntime(runtime, policies); err != nil {
if err := runtime.Initialize(policies); err != nil {
recordDockerPortGuardReconcileError(err)
return err
}
@@ -283,159 +203,48 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
}
}
func (s *DockerPortGuardService) QueueInitialization(
request dto.DockerPortGuardOperation,
) (dto.FilterChainOperationResponse, error) {
if request.Operation != "initialize" {
return dto.FilterChainOperationResponse{}, fmt.Errorf("only Docker port guard initialization can be queued")
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskDocker, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create Docker port guard initialization task: %w", err)
}
var runtime dockerGuardRuntime
var backend string
var policies []docker_guard.Policy
taskItem.AddSubTask(agenti18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func(t *task.Task) error {
var err error
runtime, backend, err = s.runtimeForDocker(t.TaskCtx)
if err != nil {
return err
}
policies, err = s.runtimePolicies(t.TaskCtx)
if err != nil {
return err
}
t.Logf("backend=%s", backend)
return nil
}, nil)
taskItem.AddSubTask(agenti18n.GetWithName("FirewallInitializeDockerGuardStep", "Docker"), func(t *task.Task) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
t.Logf("backend=%s", backend)
err := initializeDockerGuardRuntime(runtime, policies)
recordDockerPortGuardReconcileError(err)
return err
}, nil)
taskItem.AddSubTask(agenti18n.GetMsgByKey("FirewallPersistDockerGuardStep"), func(t *task.Task) error {
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
return err
}
recordDockerPortGuardReconcileError(nil)
return nil
}, nil)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("save Docker port guard initialization task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
func (s *DockerPortGuardService) DeletePolicies(ctx context.Context, request dto.DockerPortGuardPolicyBatchDelete) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
uuids, err := docker_guard.NormalizePolicyUUIDs(request.UUIDs)
if err != nil {
return dto.FilterChainOperationResponse{}, err
return err
}
labels := make([]string, len(uuids))
for i, id := range uuids {
labels[i] = fmt.Sprintf("[%d/%d] %s", i+1, len(uuids), id)
if err := s.policies.DeleteBatch(ctx, uuids); err != nil {
return err
}
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, labels, func(ctx context.Context) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
if err := s.policies.DeleteBatch(ctx, uuids); err != nil {
return err
}
return s.reconcileLocked(ctx)
})
return s.reconcileLocked(ctx)
}
func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error) {
labels := make([]string, len(request.Policies))
for i, policy := range request.Policies {
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s:%d %s", i+1, len(request.Policies), policy.Family, policy.Protocol, policy.HostIP, policy.HostPort, policy.Mode)
}
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, labels, func(ctx context.Context) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
policies := make([]model.DockerPortGuardPolicy, 0, len(request.Policies))
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
for i, policy := range request.Policies {
if err := ctx.Err(); err != nil {
return err
}
normalized, err := docker_guard.NormalizePolicy(docker_guard.Policy{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
Protocol: policy.Protocol, Mode: policy.Mode, Sources: policy.Sources,
})
if err != nil {
return fmt.Errorf("%s: %w", labels[i], err)
}
encoded, err := json.Marshal(normalized.Sources)
if err != nil {
return fmt.Errorf("%s: %w", labels[i], err)
}
policies = append(policies, model.DockerPortGuardPolicy{
UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP,
HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode,
Sources: string(encoded), Description: strings.TrimSpace(policy.Description),
})
endpoints = append(endpoints, policy.DockerPortGuardEndpointIdentity)
}
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
func (s *DockerPortGuardService) UpsertPolicies(ctx context.Context, request dto.DockerPortGuardPolicyBatch) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
policies := make([]model.DockerPortGuardPolicy, 0, len(request.Endpoints))
seen := make(map[string]struct{}, len(request.Endpoints))
for _, endpoint := range request.Endpoints {
normalized, err := docker_guard.NormalizePolicy(docker_guard.Policy{
Family: endpoint.Family, HostIP: endpoint.HostIP, HostPort: endpoint.HostPort,
Protocol: endpoint.Protocol, Mode: request.Mode, Sources: request.Sources,
})
if err != nil {
return err
}
if err := s.policies.UpsertBatch(ctx, policies); err != nil {
return err
key := guardEndpointKey(normalized.Family, normalized.HostIP, normalized.HostPort, normalized.Protocol)
if _, exists := seen[key]; exists {
continue
}
return s.reconcileLocked(ctx)
})
}
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(
ctx context.Context,
requested []dto.DockerPortGuardEndpointIdentity,
) error {
if s.client == nil || len(requested) == 0 {
return nil
seen[key] = struct{}{}
encoded, _ := json.Marshal(normalized.Sources)
policies = append(policies, model.DockerPortGuardPolicy{
UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP,
HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode,
Sources: string(encoded), Description: strings.TrimSpace(request.Description),
})
}
cli, err := s.client()
if err != nil {
return nil
if err := s.policies.UpsertBatch(ctx, policies); err != nil {
return err
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return nil
}
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
if err != nil {
return nil
}
annotateDockerEndpointManagement(endpoints, dockerFirewallBackend(info))
targets := make(map[string]string, len(endpoints))
for _, endpoint := range endpoints {
targets[guardEndpointKey(endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
}
for _, endpoint := range requested {
target := targets[guardEndpointKey(endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)]
if target == dockerManagementHostFirewall {
return fmt.Errorf("%w: endpoint traffic is handled by the host input firewall", ErrDockerGuardInvalid)
}
if target == dockerManagementNeedsDiagnosis {
return fmt.Errorf("%w: endpoint traffic management target requires diagnosis", ErrDockerGuardInvalid)
}
}
return nil
return s.reconcileLocked(ctx)
}
func (s *DockerPortGuardService) Reconcile(ctx context.Context) error {
@@ -444,6 +253,63 @@ func (s *DockerPortGuardService) Reconcile(ctx context.Context) error {
return s.reconcileLocked(ctx)
}
func (s *DockerPortGuardService) loadRuleSyncCandidates(
ctx context.Context,
request dto.FirewallRuleSyncRequest,
) (string, []model.DockerPortGuardPolicy, dockerGuardRuntime, error) {
targetProvider, err := databaseRuleSyncTarget(request, "Docker")
if err != nil {
return "", nil, nil, err
}
target := string(targetProvider)
selected, err := s.selectedRuleSyncBackend(ctx)
if err != nil {
return "", nil, nil, err
}
if target != selected {
return "", nil, nil, fmt.Errorf(
"%w: selected Docker firewall backend is %s, requested target is %s",
filter.ErrProviderUnavailable, selected, target,
)
}
policies, err := s.policies.List(ctx)
if err != nil {
return "", nil, nil, err
}
return target, policies, s.guardRuntime(target), nil
}
func (s *DockerPortGuardService) selectedRuleSyncBackend(ctx context.Context) (string, error) {
if global.DB != nil {
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
selected = strings.ToLower(strings.TrimSpace(selected))
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected, nil
}
}
if s.client == nil {
return "", fmt.Errorf("%w: Docker firewall backend is unavailable", ErrDockerUnavailable)
}
cli, err := s.client()
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
}
return selectedDockerFirewallBackend(dockerFirewallBackend(info)), nil
}
func dockerGuardPoliciesFromModels(policies []model.DockerPortGuardPolicy) []docker_guard.Policy {
result := make([]docker_guard.Policy, 0, len(policies))
for _, policy := range policies {
result = append(result, dockerGuardPolicyFromModel(policy))
}
return result
}
func dockerGuardPolicyFromModel(policy model.DockerPortGuardPolicy) docker_guard.Policy {
return docker_guard.Policy{
UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
@@ -451,46 +317,18 @@ func dockerGuardPolicyFromModel(policy model.DockerPortGuardPolicy) docker_guard
}
}
func dockerGuardReadOnlyPolicyUUID(policy docker_guard.ReadOnlyPolicy) string {
nativeRules, _ := json.Marshal(policy.NativeRules)
fingerprint := strings.Join([]string{
policy.Policy.Family, policy.Policy.HostIP, strconv.Itoa(int(policy.Policy.HostPort)),
policy.Policy.Protocol, policy.Action, string(nativeRules),
}, "\x00")
return uuid.NewSHA1(uuid.NameSpaceOID, []byte(fingerprint)).String()
func dockerGuardRuleSyncDTO(policy model.DockerPortGuardPolicy) *dto.DockerPortGuardEndpoint {
return &dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description,
}
}
func dockerGuardRuntimeReadOnlyModels(policies []docker_guard.ReadOnlyPolicy) ([]model.DockerPortGuardPolicy, error) {
result := make([]model.DockerPortGuardPolicy, 0, len(policies))
for _, policy := range policies {
sources, err := json.Marshal(policy.Policy.Sources)
if err != nil {
return nil, err
}
nativeRules, err := json.Marshal(policy.NativeRules)
if err != nil {
return nil, err
}
result = append(result, model.DockerPortGuardPolicy{
UUID: dockerGuardReadOnlyPolicyUUID(policy),
ReadOnly: true,
Family: policy.Policy.Family, HostIP: policy.Policy.HostIP, HostPort: policy.Policy.HostPort,
Protocol: policy.Policy.Protocol, Sources: string(sources), NativeAction: policy.Action,
NativeRules: string(nativeRules), Sequence: policy.Sequence,
})
func dockerGuardRuntimeRuleSyncDTO(policy docker_guard.Policy) *dto.DockerPortGuardEndpoint {
return &dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: append([]string(nil), policy.Sources...),
}
return result, nil
}
func (s *DockerPortGuardService) replaceRuntimeReadOnlyPolicies(
ctx context.Context,
policies []docker_guard.ReadOnlyPolicy,
) error {
stored, err := dockerGuardRuntimeReadOnlyModels(policies)
if err != nil {
return err
}
return s.policies.ReplaceRuntimeReadOnly(ctx, stored)
}
func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) (err error) {
@@ -521,22 +359,12 @@ func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) (err error
if err != nil {
return err
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
if err := s.replaceRuntimeReadOnlyPolicies(ctx, inventory.ReadOnly); err != nil {
return err
}
if !initialized {
err = initializeDockerGuardRuntime(runtime, policies)
err = runtime.Initialize(policies)
} else {
err = runtime.Reconcile(policies)
}
if err != nil {
return err
}
return docker_guard.Verify(runtime, policies, inventory.ReadOnly)
return err
}
func dockerPortGuardPersistedEnabled() (bool, error) {
@@ -604,7 +432,7 @@ func markDockerGuardFamilyNotEffective(base *dto.DockerPortGuardBase, family str
}
func (s *DockerPortGuardService) runtimePolicies(ctx context.Context) ([]docker_guard.Policy, error) {
stored, err := s.policies.ListManaged(ctx)
stored, err := s.policies.List(ctx)
if err != nil {
return nil, err
}
@@ -694,8 +522,8 @@ func dockerFirewallVersion(backend string) string {
return version
}
func discoverDockerEndpoints(ctx context.Context, cli *client.Client, all bool) ([]dto.DockerPortGuardEndpoint, error) {
containers, err := cli.ContainerList(ctx, containertypes.ListOptions{All: all})
func discoverDockerEndpoints(ctx context.Context, cli *client.Client) ([]dto.DockerPortGuardEndpoint, error) {
containers, err := cli.ContainerList(ctx, containertypes.ListOptions{All: true})
if err != nil {
return nil, err
}
@@ -718,7 +546,7 @@ func discoverDockerEndpoints(ctx context.Context, cli *client.Client, all bool)
} else if hostIP == "" {
hostIP = "0.0.0.0"
}
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{Family: family, HostIP: hostIP, HostPort: port.PublicPort, Protocol: port.Type, ContainerID: item.ID, ContainerName: name, ContainerState: item.State, ContainerPort: port.PrivatePort, Compose: compose, Application: application, Sources: []string{}})
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{Family: family, HostIP: hostIP, HostPort: port.PublicPort, Protocol: port.Type, ContainerID: item.ID, ContainerName: name, ContainerPort: port.PrivatePort, Compose: compose, Application: application, Sources: []string{}})
}
}
return endpoints, nil
@@ -730,8 +558,7 @@ func dockerGuardPolicyEndpoints(policies []model.DockerPortGuardPolicy) []dto.Do
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources),
Description: policy.Description, TrafficPath: dockerTrafficPathUnknown,
ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath,
Description: policy.Description,
})
}
return endpoints
@@ -767,7 +594,7 @@ func groupDockerGuardContainers(endpoints []dto.DockerPortGuardEndpoint) []dto.D
for i, endpoint := range container.Endpoints {
sources := append([]string(nil), endpoint.Sources...)
sort.Strings(sources)
policyKey := fmt.Sprintf("%t|%s|%s|%t|%s|%s|%s", endpoint.PolicyUUID != "", endpoint.Mode, strings.Join(sources, ","), endpoint.Effective, endpoint.Description, endpoint.ManagementTarget, endpoint.ManagementReason)
policyKey := fmt.Sprintf("%t|%s|%s|%t|%s", endpoint.PolicyUUID != "", endpoint.Mode, strings.Join(sources, ","), endpoint.Effective, endpoint.Description)
items = append(items, docker.PortRangeItem{
Key: endpoint.Family + "|" + endpoint.HostIP + "|" + endpoint.Protocol + "|" + policyKey,
PublicPort: endpoint.HostPort, PrivatePort: endpoint.ContainerPort,
@@ -812,256 +639,3 @@ func firstGuardString(values []string) string {
}
return values[0]
}
func annotateDockerEndpointManagement(endpoints []dto.DockerPortGuardEndpoint, backend string) {
rules := map[string]dockerForwardRules{
constant.FirewallFamilyIPv4: loadDockerDNATRules(backend, constant.FirewallFamilyIPv4),
constant.FirewallFamilyIPv6: loadDockerDNATRules(backend, constant.FirewallFamilyIPv6),
}
proxies := loadDockerProxyEndpoints()
for i := range endpoints {
familyRules := rules[endpoints[i].Family]
endpoints[i].TrafficPath, endpoints[i].ManagementTarget, endpoints[i].ManagementReason =
dockerEndpointManagement(backend, familyRules, proxies, endpoints[i])
}
}
func dockerEndpointManagement(
backend string,
rules dockerForwardRules,
proxies dockerProxyEndpoints,
endpoint dto.DockerPortGuardEndpoint,
) (string, string, string) {
if !rules.inspected {
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATInspectFailed
}
dnatMatched := dockerDNATRuleMatches(backend, rules.output, endpoint)
if dnatMatched && dockerDNATIngressReachable(backend, rules.output) {
return dockerTrafficPathForward, dockerManagementContainerGuard, ""
}
if !proxies.inspected {
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonProxyInspectFailed
}
if dockerProxyEndpointMatches(proxies.items, endpoint) {
return dockerTrafficPathInput, dockerManagementHostFirewall, ""
}
if dnatMatched {
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATChainUnreachable
}
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNoMatchingPath
}
func loadDockerDNATRules(backend, family string) dockerForwardRules {
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
if backend == constant.FirewallProviderNftables {
tableFamily := "ip"
if family == constant.FirewallFamilyIPv6 {
tableFamily = "ip6"
}
tables, err := manager.RunWithOptionalSudoAndStdout("nft", "list", "tables")
if err != nil {
return dockerForwardRules{}
}
if !strings.Contains(tables, "table "+tableFamily+" docker-bridges") {
return dockerForwardRules{inspected: true}
}
output, err := manager.RunWithOptionalSudoAndStdout("nft", "list", "table", tableFamily, "docker-bridges")
return dockerForwardRules{output: output, inspected: err == nil}
}
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return dockerForwardRules{}
}
executable := commands.IPv4
if family == constant.FirewallFamilyIPv6 {
executable = commands.IPv6
}
if executable == "" {
return dockerForwardRules{}
}
output, err := manager.RunWithOptionalSudoAndStdout(executable, "-w", "-t", "nat", "-S")
return dockerForwardRules{output: output, inspected: err == nil}
}
func loadDockerProxyEndpoints() dockerProxyEndpoints {
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=")
if err != nil {
return dockerProxyEndpoints{}
}
return dockerProxyEndpoints{items: parseDockerProxyEndpoints(output), inspected: true}
}
func parseDockerProxyEndpoints(output string) []dockerProxyEndpoint {
result := make([]dockerProxyEndpoint, 0)
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) == 0 || !dockerProxyCommand(fields) {
continue
}
protocol := commandFlagValue(fields, "-proto")
hostIP := commandFlagValue(fields, "-host-ip")
hostPortValue := commandFlagValue(fields, "-host-port")
hostPort, err := strconv.ParseUint(hostPortValue, 10, 16)
if err != nil || (protocol != "tcp" && protocol != "udp") || hostIP == "" {
continue
}
result = append(result, dockerProxyEndpoint{protocol: protocol, hostIP: canonicalAddress(hostIP), hostPort: uint16(hostPort)})
}
return result
}
func dockerProxyCommand(fields []string) bool {
for _, field := range fields {
if filepath.Base(field) == "docker-proxy" {
return true
}
}
return false
}
func commandFlagValue(fields []string, name string) string {
for i := 0; i < len(fields); i++ {
if fields[i] == name && i+1 < len(fields) {
return fields[i+1]
}
if strings.HasPrefix(fields[i], name+"=") {
return strings.TrimPrefix(fields[i], name+"=")
}
}
return ""
}
func dockerProxyEndpointMatches(proxies []dockerProxyEndpoint, endpoint dto.DockerPortGuardEndpoint) bool {
for _, proxy := range proxies {
if proxy.protocol == endpoint.Protocol && proxy.hostPort == endpoint.HostPort && hostAddressMatches(proxy.hostIP, endpoint.HostIP, endpoint.Family) {
return true
}
}
return false
}
func dockerDNATRuleMatches(backend, output string, endpoint dto.DockerPortGuardEndpoint) bool {
if strings.TrimSpace(output) == "" {
return false
}
if backend == constant.FirewallProviderNftables {
return nftDNATRuleMatches(output, endpoint)
}
return iptablesDNATRuleMatches(output, endpoint)
}
func dockerDNATIngressReachable(backend, output string) bool {
if backend == constant.FirewallProviderNftables {
return strings.Contains(output, "hook prerouting")
}
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) >= 4 && fields[0] == "-A" && fields[1] == "PREROUTING" && commandFlagValue(fields, "-j") == "DOCKER" {
return true
}
}
return false
}
func iptablesDNATRuleMatches(output string, endpoint dto.DockerPortGuardEndpoint) bool {
port := strconv.Itoa(int(endpoint.HostPort))
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if commandFlagValue(fields, "-p") != endpoint.Protocol || commandFlagValue(fields, "--dport") != port || commandFlagValue(fields, "-j") != "DNAT" {
continue
}
if destinationAddressMatches(commandFlagValue(fields, "-d"), endpoint) {
return true
}
}
return false
}
func nftDNATRuleMatches(output string, endpoint dto.DockerPortGuardEndpoint) bool {
port := strconv.Itoa(int(endpoint.HostPort))
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(strings.NewReplacer("{", " ", "}", " ", ",", " ", ";", " ").Replace(line))
if !containsToken(fields, "dnat") || !nftProtocolPortMatches(fields, endpoint.Protocol, port) {
continue
}
destination := nftDestinationAddress(fields, endpoint.Family)
if destinationAddressMatches(destination, endpoint) {
return true
}
}
return false
}
func nftProtocolPortMatches(fields []string, protocol, port string) bool {
for i := 0; i+2 < len(fields); i++ {
if fields[i] == protocol && fields[i+1] == "dport" && fields[i+2] == port {
return true
}
if fields[i] == "th" && fields[i+1] == "dport" && fields[i+2] == port && nftMetaProtocolMatches(fields, protocol) {
return true
}
}
return false
}
func nftMetaProtocolMatches(fields []string, protocol string) bool {
for i := 0; i+2 < len(fields); i++ {
if fields[i] == "meta" && fields[i+1] == "l4proto" && fields[i+2] == protocol {
return true
}
}
return false
}
func nftDestinationAddress(fields []string, family string) string {
token := "ip"
if family == constant.FirewallFamilyIPv6 {
token = "ip6"
}
for i := 0; i+2 < len(fields); i++ {
if fields[i] == token && fields[i+1] == "daddr" {
return fields[i+2]
}
}
return ""
}
func destinationAddressMatches(ruleAddress string, endpoint dto.DockerPortGuardEndpoint) bool {
ruleAddress = strings.TrimSpace(strings.Split(ruleAddress, "/")[0])
if isWildcardHostAddress(endpoint.HostIP, endpoint.Family) {
return ruleAddress == ""
}
return ruleAddress == "" || canonicalAddress(ruleAddress) == canonicalAddress(endpoint.HostIP)
}
func hostAddressMatches(left, right, family string) bool {
if isWildcardHostAddress(left, family) && isWildcardHostAddress(right, family) {
return true
}
return canonicalAddress(left) == canonicalAddress(right)
}
func isWildcardHostAddress(value, family string) bool {
value = strings.TrimSpace(value)
if family == constant.FirewallFamilyIPv6 {
return value == "" || value == "::"
}
return value == "" || value == "0.0.0.0"
}
func canonicalAddress(value string) string {
if address, err := netip.ParseAddr(strings.TrimSpace(value)); err == nil {
return address.String()
}
return strings.TrimSpace(value)
}
func containsToken(fields []string, value string) bool {
for _, field := range fields {
if field == value {
return true
}
}
return false
}
-76
View File
@@ -1,76 +0,0 @@
package service
import (
"context"
"fmt"
"io"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
)
const (
firewallTaskHost = "FirewallTaskHost"
firewallTaskForwarding = "FirewallTaskForwarding"
firewallTaskDocker = "FirewallTaskDocker"
)
func firewallTaskName(operation, subsystem, backend string) string {
name := i18n.GetMsgByKey(subsystem)
if backend != "" {
name += " · " + backend
}
key := "FirewallRule" + operation
if operation == task.TaskExec {
key = "FirewallTaskInitialize"
}
return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name})
}
func queueFirewallRuleTask(subsystem, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) {
taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
t.Logf("rules=%d", len(labels))
err := t.TaskCtx.Err()
if err == nil {
err = apply(t.TaskCtx)
}
succeeded, failed := 0, 0
for _, label := range labels {
if err != nil {
failed++
t.LogFailedWithErr(label, err)
} else {
succeeded++
t.LogSuccess(label)
}
}
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func closeUnstartedFirewallTask(t *task.Task) {
if cancel, ok := global.LoadTaskCancel(t.TaskID); ok {
cancel()
}
global.RemoveTaskCancel(t.TaskID)
if closer, ok := t.Logger.Out.(io.Closer); ok {
_ = closer.Close()
}
}
+2 -10
View File
@@ -33,8 +33,8 @@ func configuredDockerFirewallBackend() string {
}
func selectedSystemFirewallClient() (lifecycle.Client, error) {
if provider := configuredSystemFirewallBackend(); provider != "" {
return lifecycle.NewClientFor(provider)
if provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey); strings.TrimSpace(provider) != "" {
return lifecycle.NewClientFor(strings.TrimSpace(provider))
}
client, err := lifecycle.NewClient()
if err != nil {
@@ -44,14 +44,6 @@ func selectedSystemFirewallClient() (lifecycle.Client, error) {
return client, nil
}
func configuredSystemFirewallBackend() string {
if global.DB == nil {
return ""
}
provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
return strings.TrimSpace(provider)
}
func NewSelectedSystemFirewallClient() (lifecycle.Client, error) {
return selectedSystemFirewallClient()
}
+52 -289
View File
@@ -2,244 +2,45 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"slices"
"sync"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
)
type IFirewallSettingService interface {
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
Load(context.Context) (dto.FirewallSettings, error)
Operate(context.Context, dto.FirewallBackendOperation) error
}
type FirewallSettingService struct{}
var firewallWhitelistMu sync.Mutex
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required")
func firewallBackendCleanupRequired(current, target string) error {
return fmt.Errorf(
"%w: current backend %s still contains 1Panel runtime rules; clean it up before switching to %s",
ErrFirewallBackendCleanupRequired,
current,
target,
)
}
func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{}
}
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
return append(current, request.Rule), nil
})
}
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, request.OldRule)
if err != nil {
return nil, err
}
current[index] = request.Rule
return current, nil
})
}
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
if request.Rule == nil {
return fmt.Errorf("select one firewall port whitelist rule to delete")
}
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, *request.Rule)
if err != nil {
return nil, err
}
return slices.Delete(current, index, index+1), nil
})
}
func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) {
index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool {
return samePortWhitelistRule(rule, target)
})
if index < 0 {
return -1, fmt.Errorf("firewall port whitelist rule has changed or no longer exists; refresh and retry")
}
return index, nil
}
func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
if reflect.DeepEqual(left, right) {
return true
}
normalizedLeft, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{left})
if err != nil {
return false
}
normalizedRight, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{right})
if err != nil {
return false
}
slices.Sort(normalizedLeft[0].Sources)
slices.Sort(normalizedRight[0].Sources)
return reflect.DeepEqual(normalizedLeft[0], normalizedRight[0])
}
func savePortWhitelist(ctx context.Context, change func([]firewall.PortWhitelist) ([]firewall.PortWhitelist, error)) error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
defer filterruntime.InvalidateInventory()
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
current, err := loadPortWhitelistSetting(tx)
if err != nil {
return err
}
desired, err := change(current)
if err != nil {
return err
}
desired, err = firewall.ValidatePortWhitelist(desired)
if err != nil {
return err
}
value, err := json.Marshal(desired)
if err != nil {
return err
}
return tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
})
}
func checkFirewallRuleWhitelistProtection(provider filter.Provider, record model.FirewallRule) error {
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
rules, err := record.RulesForProvider(provider)
if err != nil {
return err
}
for _, rule := range rules {
if filter.RuleMatchesPortWhitelist(rule, ports) {
return filter.ErrProtectedRule
}
}
return nil
}
func loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) {
var setting model.Setting
if err := db.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error; errors.Is(err, gorm.ErrRecordNotFound) {
setting.Value = constant.FirewallPortWhiteListValue
} else if err != nil {
return nil, err
}
var rules []firewall.PortWhitelist
err := json.Unmarshal([]byte(setting.Value), &rules)
return rules, err
}
func loadSSHWhitelistPortFrom(path string) (string, error) {
directives, _, err := parseSSHConfigTree(path)
if errors.Is(err, os.ErrNotExist) {
return defaultSSHPort, nil
}
if err != nil {
return "", err
}
return loadSSHPortValues(directives)[0], nil
}
func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist {
result := make([]firewall.PortWhitelist, 0, len(entries))
for _, entry := range entries {
if entry.Type == "" {
result = append(result, entry)
}
}
return result
}
func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
entries = slices.Clone(entries)
var sshPort string
for i := range entries {
entry := &entries[i]
if entry.Type == "" || entry.Port != "" {
continue
}
switch entry.Type {
case firewall.PortWhitelistTypePanel:
entry.Port = LoadPanelPort()
case firewall.PortWhitelistTypeSSH:
if sshPort == "" {
var err error
sshPort, err = loadSSHWhitelistPortFrom(sshPath)
if err != nil {
return nil, err
}
}
entry.Port = sshPort
}
}
return firewall.ValidatePortWhitelist(entries)
}
func updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
return savePortWhitelist(ctx, func(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
for i := range entries {
if entries[i].Type == serviceType {
if len(ports) == 0 {
return nil, fmt.Errorf("firewall whitelist %s requires a port", serviceType)
}
entries[i].Port = ports[0]
}
}
return entries, nil
})
}
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()}
result := dto.FirewallSettings{PingStatus: ping.LoadStatus()}
if ports, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList); err == nil {
result.PortWhitelist = ports
} else {
result.PortWhitelist = constant.FirewallPortWhiteListValue
}
installed := make(map[string]bool)
for _, name := range lifecycle.InstalledProviders() {
installed[name] = true
}
result.System.Selected = configuredSystemFirewallBackend()
if result.System.Selected == "" {
if client, err := lifecycle.NewClient(); err == nil {
result.System.Selected = client.Name()
}
}
result.System.Current = result.System.Selected
for _, name := range []string{
constant.FirewallProviderFirewalld,
constant.FirewallProviderUFW,
@@ -247,7 +48,7 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
constant.FirewallProviderNftables,
} {
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
if option.Installed && name == result.System.Selected {
if option.Installed {
client, err := lifecycle.NewClientFor(name)
if err != nil {
option.Message = err.Error()
@@ -262,19 +63,25 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
option.Message = err.Error()
}
}
if name == result.System.Selected && name == constant.FirewallProviderIptables {
if name == constant.FirewallProviderIptables {
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil {
option.Implementation = commands.IPv4
}
}
result.System.Options = append(result.System.Options, option)
}
result.System.Selected, _ = settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
if result.System.Selected == "" {
if client, err := lifecycle.NewClient(); err == nil {
result.System.Selected = client.Name()
}
}
result.System.Current = result.System.Selected
result.Forwarding.Selected = configuredForwardingBackend()
result.Forwarding.Current = result.Forwarding.Selected
initializedForwarding := make([]string, 0, 2)
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
if option.Installed && name == result.Forwarding.Selected {
if option.Installed {
manager, err := newForwardingManagerFor(name)
if err != nil {
option.Message = err.Error()
@@ -300,19 +107,27 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
}
}
}
if name == result.Forwarding.Selected && name == constant.FirewallProviderIptables {
if option.IPv4.Initialized || option.IPv6.Initialized {
initializedForwarding = append(initializedForwarding, name)
}
if name == constant.FirewallProviderIptables {
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil {
option.Implementation = commands.IPv4
}
}
result.Forwarding.Options = append(result.Forwarding.Options, option)
}
if len(initializedForwarding) == 1 {
result.Forwarding.Current = initializedForwarding[0]
} else if len(initializedForwarding) > 1 {
result.Forwarding.Current = strings.Join(initializedForwarding, " + ")
}
result.Forwarding.Selected, _ = settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
if result.Forwarding.Selected == "" {
result.Forwarding.Selected = constant.FirewallProviderIptables
}
dockerInstalled := cmd.Which("docker")
dockerVersion := ""
if dockerInstalled {
dockerVersion = loadDockerEngineVersion(ctx)
}
result.Docker.Selected = configuredDockerFirewallBackend()
result.Docker.Current = result.Docker.Selected
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
@@ -320,37 +135,19 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
Name: name, Installed: installed[name], Supported: dockerInstalled,
Active: dockerInstalled && installed[name] && result.Docker.Selected == name,
}
if name == constant.FirewallProviderNftables && dockerInstalled && !dockerNftablesSupported(dockerVersion) {
option.Supported = false
option.SupportReason = "docker_version_unsupported"
option.Active = false
}
if option.Active {
guard := docker_guard.NewRuntime(name)
ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6)
option.Initialized = ipv4.Initialized || ipv6.Initialized
option.Bound = ipv4.Bound || ipv6.Bound
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing
option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
}
guard := docker_guard.NewRuntime(name)
ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6)
option.Initialized = ipv4.Initialized || ipv6.Initialized
option.Bound = ipv4.Bound || ipv6.Bound
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing
option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
result.Docker.Options = append(result.Docker.Options, option)
}
var err error
result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
if err != nil {
return result, err
}
result.PanelPort = LoadPanelPort()
sshPort, sshErr := loadSSHWhitelistPortFrom(sshPath)
if sshErr != nil {
global.LOG.Warnf("load SSH port for firewall settings: %v", sshErr)
} else {
result.SSHPort = sshPort
}
return result, err
return result, nil
}
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
@@ -380,10 +177,6 @@ func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFa
}
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
defer firewallLifecycleTaskMu.Unlock()
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
}
@@ -392,16 +185,7 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
}
switch request.Subsystem {
case "system":
if err := s.operateSystem(request); err != nil {
return err
}
if request.Operation == "initialize" {
service := newFirewallService()
rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
whitelistErr := service.SyncPortWhitelist(ctx)
return errors.Join(rulesErr, whitelistErr)
}
return nil
return s.operateSystem(request)
case "forwarding":
return s.operateForwarding(request)
case "docker":
@@ -430,18 +214,12 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.
return err
}
if current != request.Backend && initialized {
return firewallBackendCleanupRequired(current, request.Backend)
return fmt.Errorf("clean up the current Docker firewall backend %s before switching to %s", current, request.Backend)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
return err
}
if request.Operation == "select" {
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
if request.Operation == "initialize" {
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
@@ -466,8 +244,6 @@ func dockerGuardBackendInitialized(backend string) (bool, error) {
}
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if _, err := lifecycle.NewClientFor(request.Backend); err != nil {
return err
}
@@ -486,7 +262,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
return err
}
if initialized {
return firewallBackendCleanupRequired(previous, request.Backend)
return fmt.Errorf("clean up the current system firewall backend %s before switching to %s", previous, request.Backend)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil {
@@ -502,7 +278,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
if request.Operation == "select" {
return nil
}
initErr := newFirewallService().operateFilterChainBaseLocked(request.Backend, dto.FilterChainOperation{
initErr := newFirewallService().OperateFilterChain(dto.FilterChainOperation{
Name: constant.FirewallBasicChain, Operate: string(firewall.BaseOperationInit),
})
if initErr != nil {
@@ -512,26 +288,9 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
}
func systemFirewallBackendInitialized(backend string) (bool, error) {
return systemFirewallBackendInitializedWithClientFactory(backend, lifecycle.NewClientFor)
}
func systemFirewallBackendInitializedWithClientFactory(
backend string,
newClient func(string) (lifecycle.Client, error),
) (bool, error) {
client, err := newClient(backend)
if err != nil {
if errors.Is(err, lifecycle.ErrNotInstalled) {
return false, nil
}
return false, err
}
if supportsManagedFilterChains(backend) {
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil {
return false, err
}
@@ -541,6 +300,10 @@ func systemFirewallBackendInitializedWithClientFactory(
}
return false, nil
}
client, err := lifecycle.NewClientFor(backend)
if err != nil {
return false, err
}
return client.Status()
}
@@ -596,7 +359,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
return err
}
if current != request.Backend && initialized {
return firewallBackendCleanupRequired(current, request.Backend)
return fmt.Errorf("clean up the current forwarding backend %s before switching to %s", current, request.Backend)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
File diff suppressed because it is too large Load Diff
+178 -111
View File
@@ -11,33 +11,39 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
forwardingproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding/providers"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
)
type IForwardingService interface {
LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
SearchRules(request dto.ForwardRuleSearch) (int64, interface{}, error)
OperateRules(request dto.ForwardRuleOperate) error
Enable() error
QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error)
Restore(context.Context) error
}
type ForwardingService struct {
managerFactory func() (*forwarding.Manager, error)
rules repo.IForwardingRuleRepo
enabled func() (bool, error)
persistBackend func(string) error
markEnabled func() error
managerFactory func() (*forwarding.Manager, error)
rules repo.IForwardingRuleRepo
enabled func() (bool, error)
persistBackend func(string) error
markEnabled func() error
installedProviders func() []string
}
type forwardingCandidate struct {
adapter forwarding.Adapter
runtime forwarding.RuntimeClient
}
var errForwardingBackendConflict = errors.New("iptables and nftables forwarding backends are both initialized; remove one before continuing")
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
var forwardingMutationMu sync.Mutex
@@ -67,18 +73,33 @@ func newForwardingService() *ForwardingService {
persistBackend: func(backend string) error {
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
},
installedProviders: lifecycle.InstalledProviders,
}
}
type forwardingRuleSyncCandidate struct {
rule forwarding.Rule
err error
}
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
selected := configuredForwardingBackend()
baseInfo := dto.FirewallSubsystemStatus{
Version: "-", Name: forwardingDisplayName(selected), Backend: selected, SyncError: lastForwardingSyncError(),
Version: "-", Name: "-", Backend: "-", SyncError: lastForwardingSyncError(),
}
manager, err := s.managerFactory()
manager, err := s.manager()
if err != nil {
if errors.Is(err, errForwardingBackendUnavailable) {
baseInfo.Reason = constant.FirewallBackendNotInstalled
loadInstalled := s.installedProviders
if loadInstalled == nil {
loadInstalled = lifecycle.InstalledProviders
}
for _, provider := range loadInstalled() {
if provider == constant.FirewallProviderIptables || provider == constant.FirewallProviderNftables {
baseInfo.IsExist = true
baseInfo.Message = err.Error()
break
}
}
return baseInfo, nil
}
return baseInfo, err
@@ -90,7 +111,7 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
baseInfo.IsExist = true
baseInfo.Name, baseInfo.Backend = forwardingDisplayName(status.Name), status.Name
baseInfo.Version = status.Version
baseInfo.PingStatus = firewall.LoadPingStatus()
baseInfo.PingStatus = ping.LoadStatus()
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
baseInfo.IPv4 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv4)
baseInfo.IPv6 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv6)
@@ -116,7 +137,7 @@ func forwardingDisplayName(backend string) string {
}
}
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, interface{}, error) {
if request.Strategy != "" {
return 0, nil, nil
}
@@ -124,7 +145,7 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
if err != nil {
return 0, nil, err
}
manager, err := s.managerFactory()
manager, err := s.manager()
if err != nil {
return 0, nil, err
}
@@ -146,9 +167,6 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
inventory = filtered
total := len(inventory)
start, end := (request.Page-1)*request.PageSize, request.Page*request.PageSize
if request.All {
start, end = 0, total
}
if start > total {
return int64(total), make([]dto.ForwardRule, 0), nil
}
@@ -191,29 +209,10 @@ func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string)
return false
}
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
labels := make([]string, len(request.Rules))
operation := task.TaskCreate
for i, rule := range request.Rules {
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s %s -> %s:%s", i+1, len(request.Rules), rule.Operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
if rule.Operation != "add" {
operation = task.TaskUpdate
}
}
if forwardingOperationsOnlyRemove(request.Rules) {
operation = task.TaskDelete
}
return queueFirewallRuleTask(firewallTaskForwarding, operation, labels, func(ctx context.Context) error {
return s.operateRules(ctx, request)
})
}
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate) error {
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
ctx := context.Background()
stored, err := s.rules.List(ctx)
if err != nil {
return err
@@ -244,7 +243,7 @@ func (s *ForwardingService) operateRules(ctx context.Context, request dto.Forwar
func (s *ForwardingService) Enable() error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
manager, err := s.managerFactory()
manager, err := s.manager()
if err != nil {
recordForwardingSyncError(err)
return err
@@ -267,58 +266,6 @@ func (s *ForwardingService) Enable() error {
return err
}
func (s *ForwardingService) QueueInitialization(
request dto.FirewallInitializationTask,
) (dto.FilterChainOperationResponse, error) {
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskForwarding, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err)
}
var manager *forwarding.Manager
var backend string
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
var err error
manager, err = s.managerFactory()
if err != nil {
recordForwardingSyncError(err)
return err
}
backend = manager.Name()
t.Logf("backend=%s", backend)
if err := s.persistForwardingEnabled(); err != nil {
recordForwardingSyncError(err)
return err
}
if err := s.activateManager(manager); err != nil {
recordForwardingSyncError(err)
return err
}
return nil
}, nil)
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func(t *task.Task) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
rules, err := s.rules.List(t.TaskCtx)
if err != nil {
recordForwardingSyncError(err)
return err
}
err = manager.Reconcile(forwardingRulesFromModels(rules))
recordForwardingSyncError(err)
return err
}, nil)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("save forwarding initialization task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func (s *ForwardingService) Restore(ctx context.Context) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
@@ -329,7 +276,7 @@ func (s *ForwardingService) Restore(ctx context.Context) error {
}
return err
}
manager, err := s.managerFactory()
manager, err := s.manager()
if err != nil {
recordForwardingSyncError(err)
return err
@@ -348,8 +295,87 @@ func (s *ForwardingService) Restore(ctx context.Context) error {
return err
}
func (s *ForwardingService) loadRuleSyncCandidates(
ctx context.Context,
targetProvider filter.Provider,
) (*forwarding.Manager, []forwardingRuleSyncCandidate, []forwarding.Rule, bool, error) {
target, err := s.manager()
if err != nil {
return nil, nil, nil, false, err
}
if target.Name() != string(targetProvider) {
return nil, nil, nil, false, fmt.Errorf(
"%w: selected forwarding backend is %s, requested target is %s",
filter.ErrProviderUnavailable, target.Name(), targetProvider,
)
}
stored, err := s.rules.List(ctx)
if err != nil {
return nil, nil, nil, false, err
}
candidates := make([]forwardingRuleSyncCandidate, 0, len(stored))
for _, record := range stored {
rule := forwarding.Rule{
Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP,
TargetPort: record.TargetPort, Interface: record.Interface,
}
normalized, normalizeErr := forwarding.NormalizeRule(rule)
candidates = append(candidates, forwardingRuleSyncCandidate{rule: normalized, err: normalizeErr})
}
targetStatus, err := target.Status()
if err != nil {
return nil, nil, nil, false, err
}
targetRules := make([]forwarding.Rule, 0)
if targetStatus.IsInit {
targetRules, err = target.List("", "")
if err != nil {
return nil, nil, nil, false, err
}
targetRules, err = normalizeForwardingRuntimeRules(targetRules)
if err != nil {
return nil, nil, nil, false, err
}
}
return target, candidates, targetRules, targetStatus.IsInit, nil
}
func verifyForwardingRuleSync(target *forwarding.Manager, desired []forwarding.Rule) error {
actual, err := target.List("", "")
if err != nil {
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
}
actual, err = normalizeForwardingRuntimeRules(actual)
if err != nil {
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
}
if !databaseSyncStatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) {
return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database")
}
return nil
}
func normalizeForwardingRuntimeRules(rules []forwarding.Rule) ([]forwarding.Rule, error) {
normalized := make([]forwarding.Rule, 0, len(rules))
for _, rule := range rules {
item, err := forwarding.NormalizeRule(rule)
if err != nil {
return nil, fmt.Errorf("normalize target forwarding rule %s: %w", rule.Identity(), err)
}
normalized = append(normalized, item)
}
return normalized, nil
}
func forwardingRuleSyncDTO(rule forwarding.Rule) *dto.ForwardRule {
return &dto.ForwardRule{
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort, Interface: rule.Interface,
}
}
func (s *ForwardingService) reconcile(rules []forwarding.Rule) error {
manager, err := s.managerFactory()
manager, err := s.manager()
if err != nil {
return err
}
@@ -548,30 +574,71 @@ func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool
return true
}
func newForwardingManager() (*forwarding.Manager, error) {
return newForwardingManagerFor(configuredForwardingBackend())
func (s *ForwardingService) manager() (*forwarding.Manager, error) {
return s.managerFactory()
}
func configuredForwardingBackend() string {
func newForwardingManager() (*forwarding.Manager, error) {
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
selected = strings.TrimSpace(selected)
if selected == "" {
return constant.FirewallProviderIptables
if strings.TrimSpace(selected) == "" {
selected = constant.FirewallProviderIptables
}
return selected
return newForwardingManagerFor(strings.TrimSpace(selected))
}
func newForwardingManagerFor(backend string) (*forwarding.Manager, error) {
client, err := lifecycle.NewClientFor(backend)
clients, err := lifecycle.NewNetfilterClients()
if err != nil {
return nil, fmt.Errorf("%w: %v", errForwardingBackendUnavailable, err)
}
candidates := make([]forwardingCandidate, 0, len(clients))
for _, client := range clients {
adapter, err := forwardingproviders.New(client.Name())
if err != nil {
return nil, err
}
candidates = append(candidates, forwardingCandidate{adapter: adapter, runtime: client})
}
if backend != "" {
for _, candidate := range candidates {
if candidate.adapter.Name() == backend {
return forwarding.NewManager(candidate.adapter, candidate.runtime), nil
}
}
return nil, fmt.Errorf(
"%w: selected forwarding backend %s: %w",
errForwardingBackendUnavailable, backend, err,
"%w: selected forwarding backend %s %w",
errForwardingBackendUnavailable, backend, lifecycle.ErrNotInstalled,
)
}
adapter, err := forwarding.New(client.Name())
if err != nil {
return nil, err
return selectForwardingManager(candidates)
}
func selectForwardingManager(candidates []forwardingCandidate) (*forwarding.Manager, error) {
if len(candidates) == 0 {
return nil, errors.New("no supported forwarding backend detected")
}
return forwarding.NewManager(adapter, client), nil
selected := -1
for index, candidate := range candidates {
ipv4Initialized, _, err := candidate.adapter.FamilyStatus(forwarding.FamilyIPv4)
if err != nil {
return nil, err
}
ipv6Initialized, _, err := candidate.adapter.FamilyStatus(forwarding.FamilyIPv6)
if err != nil {
return nil, err
}
initialized := ipv4Initialized || ipv6Initialized
if !initialized {
continue
}
if selected >= 0 {
return nil, errForwardingBackendConflict
}
selected = index
}
if selected < 0 {
selected = 0
}
candidate := candidates[selected]
return forwarding.NewManager(candidate.adapter, candidate.runtime), nil
}
+71 -23
View File
@@ -29,6 +29,7 @@ import (
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/homedir"
)
type ImageService struct{}
@@ -277,37 +278,38 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
itemName := strings.ReplaceAll(path.Base(item), ":", "_")
taskItem.AddSubTask(i18n.GetWithName("ImagePull", itemName), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", itemName)
if req.RepoID == 0 {
pullErr := pullImages(taskItem, client, item)
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), pullErr)
return pullErr
}
options := image.PullOptions{}
imageName := item
repo, repoErr := imageRepoRepo.Get(repo.WithByID(req.RepoID))
taskItem.LogWithStatus(i18n.GetMsgByKey("ImageRepoAuthFromDB"), repoErr)
if repoErr != nil {
return repoErr
}
if repo.Auth {
authConfig := registry.AuthConfig{
Username: repo.Username,
Password: repo.Password,
if req.RepoID == 0 {
hasAuth, authStr := loadAuthInfo(item)
if hasAuth {
options.RegistryAuth = authStr
}
encodedJSON, err := json.Marshal(authConfig)
} else {
repo, err := imageRepoRepo.Get(repo.WithByID(req.RepoID))
taskItem.LogWithStatus(i18n.GetMsgByKey("ImageRepoAuthFromDB"), err)
if err != nil {
return err
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
options.RegistryAuth = authStr
if repo.Auth {
authConfig := registry.AuthConfig{
Username: repo.Username,
Password: repo.Password,
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return err
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
options.RegistryAuth = authStr
}
imageName = repo.DownloadUrl + "/" + item
}
imageName = repo.DownloadUrl + "/" + item
dockerCli := docker.NewClientWithExist(client)
pullErr := dockerCli.PullImageWithProcessAndOptions(taskItem, imageName, options)
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), pullErr)
if pullErr != nil {
return pullErr
err = dockerCli.PullImageWithProcessAndOptions(taskItem, imageName, options)
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), err)
if err != nil {
return err
}
return nil
}, nil)
@@ -545,3 +547,49 @@ func checkUsed(imageID string, containers []container.Summary) bool {
}
return false
}
func loadAuthInfo(image string) (bool, string) {
if !strings.Contains(image, "/") {
return false, ""
}
homeDir := homedir.Get()
confPath := path.Join(homeDir, ".docker/config.json")
configFileBytes, err := os.ReadFile(confPath)
if err != nil {
return false, ""
}
var config dockerConfig
if err = json.Unmarshal(configFileBytes, &config); err != nil {
return false, ""
}
var (
user string
passwd string
)
imagePrefix := strings.Split(image, "/")[0]
if val, ok := config.Auths[imagePrefix]; ok {
itemByte, _ := base64.StdEncoding.DecodeString(val.Auth)
itemStr := string(itemByte)
if strings.Contains(itemStr, ":") {
user = strings.Split(itemStr, ":")[0]
passwd = strings.Split(itemStr, ":")[1]
}
}
authConfig := registry.AuthConfig{
Username: user,
Password: passwd,
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return false, ""
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
return true, authStr
}
type dockerConfig struct {
Auths map[string]authConfig `json:"auths"`
}
type authConfig struct {
Auth string `json:"auth"`
}
+2 -8
View File
@@ -32,7 +32,7 @@ type NginxService struct {
type INginxService interface {
GetNginxConfig() (*response.NginxFile, error)
GetConfigByScope(req request.NginxScopeReq) (interface{}, error)
GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error)
UpdateConfigByScope(req request.NginxConfigUpdate) error
GetStatus() (response.NginxStatus, error)
UpdateConfigFile(req request.NginxConfigFileUpdate) error
@@ -62,10 +62,7 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
return &response.NginxFile{Content: string(byteContent)}, nil
}
func (n NginxService) GetConfigByScope(req request.NginxScopeReq) (interface{}, error) {
if req.Scope == dto.Brotli {
return getNginxBrotliParams()
}
func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) {
keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 {
return nil, nil
@@ -74,9 +71,6 @@ func (n NginxService) GetConfigByScope(req request.NginxScopeReq) (interface{},
}
func (n NginxService) UpdateConfigByScope(req request.NginxConfigUpdate) error {
if req.Scope == dto.Brotli {
return updateNginxBrotliParams(getNginxParams(req.Params, dto.BrotliKeys))
}
keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 {
return nil
-168
View File
@@ -1,168 +0,0 @@
package service
import (
"os"
"path"
"regexp"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
)
// stockNginxGzipDirectives is the gzip block shipped by the OpenResty app
// since 1.21.4.3. The upgrade only rewrites values when the installed
// nginx.conf still carries exactly these directives and values, which proves
// the user never tuned compression. Any deviation aborts the rewrite.
var stockNginxGzipDirectives = map[string]string{
"gzip": "on",
"gzip_min_length": "1k",
"gzip_buffers": "4 16k",
"gzip_http_version": "1.1",
"gzip_comp_level": "2",
"gzip_types": "text/plain application/javascript application/x-javascript text/javascript text/css application/xml",
"gzip_vary": "on",
"gzip_proxied": "expired no-cache no-store private auth",
"gzip_disable": `"MSIE [1-6]\."`,
}
// correctedNginxGzipDirectives replaces the stock values in place. gzip lives
// in the http block of nginx.conf and must stay there: repeating it from an
// included file would make nginx reject the configuration with a duplicate
// directive error, and the compression settings page reads and writes these
// same keys in nginx.conf.
var correctedNginxGzipDirectives = map[string]string{
"gzip_comp_level": "5",
"gzip_types": strings.Join(nginxCompressibleTypes, " "),
"gzip_proxied": "any",
}
// obsoleteNginxGzipDirectives are dropped outright.
var obsoleteNginxGzipDirectives = map[string]struct{}{
// A per-request User-Agent regex for browsers with no measurable share.
"gzip_disable": {},
}
var nginxGzipDirectiveRe = regexp.MustCompile(`(?m)^[ \t]*(gzip[a-z_]*)[ \t]+([^;\n]*);[ \t]*$`)
func nginxMainConfigPath(install model.AppInstall) string {
return path.Join(install.GetPath(), nginxModuleConfDir, "nginx.conf")
}
// upgradeStockNginxGzipConfig rewrites the factory gzip defaults in place.
//
// Upgrades deliberately preserve the user's nginx.conf, so corrected defaults
// shipped with a new OpenResty version would otherwise never reach existing
// installations.
//
// The config parser is not used: its dumper regenerates the whole file, drops
// standalone comments and reorders proxy includes, which would be destructive
// on a user's main config. Lines are edited individually so everything outside
// the gzip block stays byte-identical.
func upgradeStockNginxGzipConfig(install model.AppInstall) error {
configPath := nginxMainConfigPath(install)
content, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if !isStockNginxGzipConfig(string(content)) {
return nil
}
updated := rewriteNginxGzipDirectives(string(content))
if updated == string(content) {
return nil
}
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
return err
}
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
return err
}
global.LOG.Info("updated the stock OpenResty gzip configuration to the current defaults")
return nil
}
// isStockNginxGzipConfig reports whether every gzip directive in the config
// matches the factory defaults exactly, with none missing and none extra.
func isStockNginxGzipConfig(content string) bool {
found := make(map[string]string)
for _, match := range nginxGzipDirectiveRe.FindAllStringSubmatch(content, -1) {
name := match[1]
value := strings.Join(strings.Fields(match[2]), " ")
if _, ok := found[name]; ok {
// A directive repeated in the http block means the config was
// edited by hand; leave it alone.
return false
}
found[name] = value
}
if len(found) != len(stockNginxGzipDirectives) {
return false
}
for name, expected := range stockNginxGzipDirectives {
if found[name] != expected {
return false
}
}
return true
}
// rewriteNginxGzipDirectives updates known values in place, drops obsolete
// directives and appends directives that are missing, preserving the original
// indentation and leaving every other line untouched.
func rewriteNginxGzipDirectives(content string) string {
lines := strings.Split(content, "\n")
result := make([]string, 0, len(lines))
seen := make(map[string]struct{})
lastGzipIndex := -1
lastGzipIndent := " "
for _, line := range lines {
match := nginxGzipDirectiveRe.FindStringSubmatch(line)
if match == nil {
result = append(result, line)
continue
}
name := match[1]
// The indentation belongs to the line itself; a top-level directive
// must not inherit the indent a previous, nested directive used.
lineIndent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
if lineIndent == "" {
lineIndent = " "
}
lastGzipIndent = lineIndent
if _, obsolete := obsoleteNginxGzipDirectives[name]; obsolete {
continue
}
seen[name] = struct{}{}
if replacement, ok := correctedNginxGzipDirectives[name]; ok {
result = append(result, lineIndent+name+" "+replacement+";")
} else {
result = append(result, line)
}
lastGzipIndex = len(result) - 1
}
// Directives introduced by a newer default set are appended right after
// the existing block so they stay visually grouped.
var missing []string
for name := range correctedNginxGzipDirectives {
if _, ok := seen[name]; !ok {
missing = append(missing, name)
}
}
if len(missing) == 0 || lastGzipIndex < 0 {
return strings.Join(result, "\n")
}
sort.Strings(missing)
added := make([]string, 0, len(missing))
for _, name := range missing {
added = append(added, lastGzipIndent+name+" "+correctedNginxGzipDirectives[name]+";")
}
tail := append(added, result[lastGzipIndex+1:]...)
return strings.Join(append(result[:lastGzipIndex+1], tail...), "\n")
}
@@ -1,182 +0,0 @@
package service
import (
"strings"
"testing"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
)
const stockNginxConf = `user root;
worker_processes auto;
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
events {
use epoll;
}
http {
include mime.types;
default_type application/octet-stream;
server_names_hash_bucket_size 512;
keepalive_requests 5000;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
limit_conn_zone $binary_remote_addr zone=perip:10m;
include /usr/local/openresty/nginx/conf/http.d/*.conf;
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
}
`
func TestIsStockNginxGzipConfig(t *testing.T) {
if !isStockNginxGzipConfig(stockNginxConf) {
t.Fatal("factory configuration should be detected as stock")
}
}
func TestIsStockNginxGzipConfigRejectsTunedValues(t *testing.T) {
cases := map[string]string{
"comp level changed": strings.Replace(stockNginxConf, "gzip_comp_level 2;", "gzip_comp_level 6;", 1),
"gzip disabled": strings.Replace(stockNginxConf, "gzip on;", "gzip off;", 1),
"types extended": strings.Replace(stockNginxConf,
"application/xml;", "application/xml application/json;", 1),
"directive removed": strings.Replace(stockNginxConf, " gzip_vary on;\n", "", 1),
"directive added": strings.Replace(stockNginxConf, " gzip_vary on;\n",
" gzip_vary on;\n gzip_static on;\n", 1),
}
for name, content := range cases {
if isStockNginxGzipConfig(content) {
t.Errorf("%s: tuned configuration must not be rewritten", name)
}
}
}
func TestIsStockNginxGzipConfigRejectsDuplicateDirective(t *testing.T) {
content := strings.Replace(stockNginxConf, " gzip on;\n", " gzip on;\n gzip on;\n", 1)
if isStockNginxGzipConfig(content) {
t.Fatal("a duplicated directive indicates a hand-edited config")
}
}
func TestRewriteNginxGzipDirectives(t *testing.T) {
result := rewriteNginxGzipDirectives(stockNginxConf)
for _, expected := range []string{
" gzip_comp_level 5;",
" gzip_proxied any;",
" gzip on;",
" gzip_vary on;",
} {
if !strings.Contains(result, expected) {
t.Errorf("expected directive missing: %s\n%s", expected, result)
}
}
if !strings.Contains(result, "application/json") {
t.Error("gzip_types should now cover application/json")
}
if strings.Contains(result, "gzip_disable") {
t.Error("obsolete gzip_disable should have been dropped")
}
if strings.Contains(result, "gzip_comp_level 2;") {
t.Error("stale comp level should have been replaced")
}
// Everything outside the gzip block must survive untouched.
for _, keep := range []string{
"server_names_hash_bucket_size 512;",
"keepalive_requests 5000;",
"limit_conn_zone $binary_remote_addr zone=perip:10m;",
"include /usr/local/openresty/nginx/conf/http.d/*.conf;",
"include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
"include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;",
"user root;",
} {
if !strings.Contains(result, keep) {
t.Errorf("unrelated line was altered or dropped: %s", keep)
}
}
if !strings.HasSuffix(result, "}\n") {
t.Error("trailing newline was not preserved")
}
}
func TestRewriteNginxGzipDirectivesIsIdempotent(t *testing.T) {
once := rewriteNginxGzipDirectives(stockNginxConf)
twice := rewriteNginxGzipDirectives(once)
if once != twice {
t.Errorf("rewrite is not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", once, twice)
}
}
func TestRewriteNginxGzipDirectivesAppendsMissing(t *testing.T) {
// gzip_proxied absent from the source must be appended, not silently lost.
content := strings.Replace(stockNginxConf,
" gzip_proxied expired no-cache no-store private auth;\n", "", 1)
result := rewriteNginxGzipDirectives(content)
if !strings.Contains(result, "gzip_proxied any;") {
t.Errorf("missing directive was not appended:\n%s", result)
}
if !strings.Contains(result, "limit_conn_zone $binary_remote_addr zone=perip:10m;") {
t.Error("appending must not clobber following lines")
}
}
func TestRewriteNginxGzipDirectivesKeepsGzipLikeNames(t *testing.T) {
// gunzip and proxy_set_header must survive: only directives whose name
// starts with "gzip" are managed here.
content := "http {\n gunzip on;\n gzip on;\n proxy_set_header Accept-Encoding gzip;\n}\n"
result := rewriteNginxGzipDirectives(content)
if !strings.Contains(result, "gunzip on;") {
t.Error("gunzip directive must be preserved")
}
if !strings.Contains(result, "proxy_set_header Accept-Encoding gzip;") {
t.Error("proxy_set_header must be preserved")
}
if !strings.Contains(result, " gzip on;") {
t.Error("gzip directive should be kept in place")
}
}
// The gzip.conf template, the upgrade maps and the appstore defaults are three
// copies of one intent. Pin the first two so they cannot drift apart silently.
func TestGzipTemplateMatchesCorrectedDefaults(t *testing.T) {
template := nginx_conf.GetWebsiteFile("gzip.conf")
if len(template) == 0 {
t.Fatal("gzip.conf template is missing from the embedded files")
}
expected := make(map[string]string, len(stockNginxGzipDirectives))
for name, value := range stockNginxGzipDirectives {
expected[name] = value
}
for name := range obsoleteNginxGzipDirectives {
delete(expected, name)
}
for name, value := range correctedNginxGzipDirectives {
expected[name] = value
}
found := make(map[string]string)
for _, match := range nginxGzipDirectiveRe.FindAllStringSubmatch(string(template), -1) {
found[match[1]] = strings.Join(strings.Fields(match[2]), " ")
}
if len(found) != len(expected) {
t.Fatalf("template has %d directives, corrected defaults have %d", len(found), len(expected))
}
for name, want := range expected {
if got, ok := found[name]; !ok {
t.Errorf("template is missing %s", name)
} else if got != want {
t.Errorf("%s: template has %q, corrected defaults have %q", name, got, want)
}
}
}
-245
View File
@@ -1,245 +0,0 @@
package service
import (
"errors"
"fmt"
"os"
"path"
"regexp"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
)
const (
// nginxHTTPConfDir holds http-context directives generated by 1Panel.
// load_module is a main-context directive and therefore lives in
// modules-enabled, which cannot host http-context directives such as
// "brotli on". The directory is included by nginx.conf before conf.d so
// that per-site configuration keeps overriding these defaults.
nginxHTTPConfDir = "http.d"
nginxHTTPConfigPrefix = "1panel-http-"
nginxHTTPConfigHeader = "# Managed by 1Panel. Manual changes will be overwritten.\n"
// nginxHTTPIncludeDirective is the include line that loads the managed
// directory. Fresh installs carry it in the shipped nginx.conf; existing
// ones get it inserted by the panel the first time a module needs
// http-context configuration.
nginxHTTPIncludeDirective = "include /usr/local/openresty/nginx/conf/http.d/*.conf;"
)
var (
// nginxHTTPIncludeRe matches the include line wherever it appears. The
// absolute path prefix, quoting and whitespace are all optional in the
// match so a variant written by an older installer or by hand still
// counts; a commented-out copy does not.
nginxHTTPIncludeRe = regexp.MustCompile(`(?m)^[ \t]*include\s+"?(/usr/local/openresty/nginx/conf/)?http\.d/\*\.conf"?\s*;[ \t]*\r?$`)
// nginxConfDIncludeRe locates the site-config include, the preferred
// insertion point, and captures its indentation.
nginxConfDIncludeRe = regexp.MustCompile(`(?m)^([ \t]*)include\s+"?(/usr/local/openresty/nginx/conf/)?conf\.d/\*\.conf"?\s*;[ \t]*\r?$`)
// nginxHTTPBlockStartRe locates the http block opening, the fallback
// insertion point, and captures its indentation.
nginxHTTPBlockStartRe = regexp.MustCompile(`(?m)^([ \t]*)http[ \t]*\{[ \t]*\r?$`)
)
// nginxHTTPIncludePresent reports whether nginx.conf already loads http.d.
func nginxHTTPIncludePresent(install model.AppInstall) bool {
content, err := os.ReadFile(nginxMainConfigPath(install))
if err != nil {
return false
}
return nginxHTTPIncludeRe.MatchString(string(content))
}
// writeNginxFileAtomic writes through a temp file plus rename so a crash or a
// concurrent reader never observes a half-written config.
func writeNginxFileAtomic(filePath string, content []byte) error {
tmpPath := filePath + ".tmp"
if err := os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
return err
}
return os.Rename(tmpPath, filePath)
}
// nginxFileLineEnding picks the file's own style so an inserted or rewritten
// line does not mix LF into a CRLF file.
func nginxFileLineEnding(content string) string {
if strings.Contains(content, "\r\n") {
return "\r\n"
}
return "\n"
}
// insertNginxHTTPInclude returns the config with the http.d include added.
//
// The include goes right before the conf.d include so panel-managed defaults
// are evaluated before per-site configuration; without one, it goes at the
// top of the http block. The inserted line follows the file's own line-ending
// style, and everything else stays byte-identical. A config without a
// locatable http block is rejected, and callers degrade instead of failing
// their operation over it.
func insertNginxHTTPInclude(content string) (string, error) {
if nginxHTTPIncludeRe.MatchString(content) {
return content, nil
}
eol := nginxFileLineEnding(content)
if m := nginxConfDIncludeRe.FindStringSubmatchIndex(content); m != nil {
indent := content[m[2]:m[3]]
return content[:m[0]] + indent + nginxHTTPIncludeDirective + eol + content[m[0]:], nil
}
if m := nginxHTTPBlockStartRe.FindStringSubmatchIndex(content); m != nil {
indent := content[m[2]:m[3]] + " "
return content[:m[1]] + eol + indent + nginxHTTPIncludeDirective + content[m[1]:], nil
}
return "", errors.New("no insertion point for the http.d include in nginx.conf")
}
// ensureNginxHTTPIncludeActive makes nginx.conf load http.d, inserting the
// include when missing. It returns whether the directory is loaded after the
// call, plus the original config content so the caller can roll back the edit
// together with the rest of its changes.
func ensureNginxHTTPIncludeActive(install model.AppInstall) (active bool, snapshot []byte, err error) {
configPath := nginxMainConfigPath(install)
content, readErr := os.ReadFile(configPath)
if readErr != nil {
return false, nil, readErr
}
if nginxHTTPIncludeRe.MatchString(string(content)) {
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
return false, nil, err
}
return true, nil, nil
}
updated, insErr := insertNginxHTTPInclude(string(content))
if insErr != nil {
return false, nil, insErr
}
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
return false, nil, err
}
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
return false, nil, err
}
return true, content, nil
}
// nginxHTTPDirective is a single http-context directive rendered into a
// managed file.
type nginxHTTPDirective struct {
Name string
Params []string
}
func (d nginxHTTPDirective) render() string {
if len(d.Params) == 0 {
return d.Name + ";"
}
return d.Name + " " + strings.Join(d.Params, " ") + ";"
}
func nginxHTTPConfigDir(install model.AppInstall) string {
return path.Join(install.GetPath(), nginxModuleConfDir, nginxHTTPConfDir)
}
func nginxHTTPConfigFileName(order int, name string) string {
return fmt.Sprintf("%s%04d-%s.conf", nginxHTTPConfigPrefix, order, nginxModulePathName(name))
}
// renderNginxHTTPConfig builds the content of a managed http.d file.
func renderNginxHTTPConfig(directives []nginxHTTPDirective) []byte {
var content strings.Builder
content.WriteString(nginxHTTPConfigHeader)
for _, directive := range directives {
content.WriteString(directive.render())
content.WriteString("\n")
}
return []byte(content.String())
}
var nginxHTTPDirectiveRe = regexp.MustCompile(`^[ \t]*([a-z_][a-z0-9_]*)[ \t]+([^;]*);[ \t]*$`)
// readNginxHTTPDirectives parses a managed file back into directive values.
// A missing or unreadable file yields no directives, which makes callers fall
// back to their defaults.
func readNginxHTTPDirectives(filePath string) map[string][]string {
content, err := os.ReadFile(filePath)
if err != nil {
return nil
}
directives := make(map[string][]string)
for _, line := range strings.Split(string(content), "\n") {
match := nginxHTTPDirectiveRe.FindStringSubmatch(line)
if match == nil {
continue
}
directives[match[1]] = strings.Fields(match[2])
}
return directives
}
// snapshotManagedNginxHTTPConfigs captures every managed file so a failed
// nginx -t can be rolled back.
func snapshotManagedNginxHTTPConfigs(configDir string) (nginxModuleConfigSnapshot, error) {
snapshot := make(nginxModuleConfigSnapshot)
entries, err := os.ReadDir(configDir)
if err != nil {
if os.IsNotExist(err) {
return snapshot, nil
}
return nil, err
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) {
continue
}
content, readErr := os.ReadFile(path.Join(configDir, entry.Name()))
if readErr != nil {
return nil, readErr
}
snapshot[entry.Name()] = content
}
return snapshot, nil
}
// applyManagedNginxHTTPConfigs writes the desired managed files and removes
// managed files that are no longer wanted. Files not carrying the managed
// prefix are never touched.
func applyManagedNginxHTTPConfigs(configDir string, desired map[string][]byte) error {
if err := os.MkdirAll(configDir, constant.DirPerm); err != nil {
return err
}
entries, err := os.ReadDir(configDir)
if err != nil {
return err
}
names := make([]string, 0, len(desired))
for fileName := range desired {
names = append(names, fileName)
}
sort.Strings(names)
for _, fileName := range names {
tmpPath := path.Join(configDir, "."+fileName+".tmp")
if err = os.WriteFile(tmpPath, desired[fileName], constant.FilePerm); err != nil {
return err
}
if err = os.Rename(tmpPath, path.Join(configDir, fileName)); err != nil {
return err
}
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) {
continue
}
if _, ok := desired[entry.Name()]; !ok {
if err = os.Remove(path.Join(configDir, entry.Name())); err != nil && !os.IsNotExist(err) {
return err
}
}
}
return nil
}
-176
View File
@@ -1,176 +0,0 @@
package service
import (
"strings"
"testing"
)
const plainNginxConf = `user root;
worker_processes auto;
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
events {
use epoll;
}
http {
include mime.types;
default_type application/octet-stream;
gzip on;
gzip_comp_level 5;
limit_conn_zone $binary_remote_addr zone=perip:10m;
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
include /usr/local/openresty/nginx/conf/default/*.conf;
}
`
func TestInsertNginxHTTPIncludeBeforeConfD(t *testing.T) {
got, err := insertNginxHTTPInclude(plainNginxConf)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, " "+nginxHTTPIncludeDirective) {
t.Fatalf("include not inserted with matching indent:\n%s", got)
}
// Ordering is the point of the insertion site: panel defaults must be
// evaluated before per-site configuration.
httpIdx := strings.Index(got, "conf/http.d/*.conf")
confDIdx := strings.Index(got, "conf/conf.d/*.conf")
if httpIdx < 0 || confDIdx < 0 || httpIdx > confDIdx {
t.Fatalf("http.d must be included before conf.d (http.d=%d conf.d=%d)", httpIdx, confDIdx)
}
// The rest of the file must be untouched.
stripped := strings.Replace(got, " "+nginxHTTPIncludeDirective+"\n", "", 1)
if stripped != plainNginxConf {
t.Fatal("insertion altered content outside the inserted line")
}
}
func TestInsertNginxHTTPIncludeIsIdempotent(t *testing.T) {
once, err := insertNginxHTTPInclude(plainNginxConf)
if err != nil {
t.Fatal(err)
}
twice, err := insertNginxHTTPInclude(once)
if err != nil {
t.Fatal(err)
}
if twice != once {
t.Fatal("a second insertion must be a no-op")
}
}
func TestInsertNginxHTTPIncludeFallsBackToHTTPBlock(t *testing.T) {
content := strings.Replace(plainNginxConf,
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;\n", "", 1)
got, err := insertNginxHTTPInclude(content)
if err != nil {
t.Fatal(err)
}
httpIdx := strings.Index(got, "http {")
incIdx := strings.Index(got, nginxHTTPIncludeDirective)
if incIdx < 0 || incIdx < httpIdx {
t.Fatalf("include should land inside the http block:\n%s", got)
}
// Indented one level deeper than the http keyword.
if !strings.Contains(got, " "+nginxHTTPIncludeDirective) {
t.Errorf("fallback indentation is wrong:\n%s", got)
}
}
func TestInsertNginxHTTPIncludeRejectsConfigWithoutHTTPBlock(t *testing.T) {
if _, err := insertNginxHTTPInclude("events {}\n"); err == nil {
t.Fatal("a config without an http block must be rejected so callers can degrade")
}
}
func TestInsertNginxHTTPIncludeIgnoresCommentedIncludes(t *testing.T) {
commented := strings.Replace(plainNginxConf,
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
" # include /usr/local/openresty/nginx/conf/conf.d/*.conf;", 1)
got, err := insertNginxHTTPInclude(commented)
if err != nil {
t.Fatal(err)
}
// The commented conf.d line is not a valid anchor; the fallback must win.
if strings.Index(got, nginxHTTPIncludeDirective) < strings.Index(got, "http {") {
t.Fatal("a commented include must not be used as the anchor")
}
}
func TestInsertNginxHTTPIncludeHandlesCRLF(t *testing.T) {
crlf := strings.ReplaceAll(plainNginxConf, "\n", "\r\n")
got, err := insertNginxHTTPInclude(crlf)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, nginxHTTPIncludeDirective) {
t.Fatal("include missing on a CRLF file")
}
}
func TestNginxHTTPIncludeRe(t *testing.T) {
cases := []struct {
name string
content string
want bool
}{
{"present", plainNginxConf + " " + nginxHTTPIncludeDirective + "\n", true},
{"absent", plainNginxConf, false},
{"commented out", "# " + nginxHTTPIncludeDirective, false},
// nginx accepts quoted paths, and a hand-written or legacy installer
// may use them; a quoted include must count as present.
{"quoted absolute path", `include "/usr/local/openresty/nginx/conf/http.d/*.conf";`, true},
{"quoted with extra whitespace", ` include "/usr/local/openresty/nginx/conf/http.d/*.conf" ;`, true},
{"relative path form", ` include http.d/*.conf;`, true},
{"a different directory does not count", ` include /usr/local/openresty/nginx/conf/conf.d/*.conf;`, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := nginxHTTPIncludeRe.MatchString(tc.content); got != tc.want {
t.Fatalf("expected %v, got %v", tc.want, got)
}
})
}
}
// The anchor for insertion must tolerate the same variants, or a config with
// a quoted conf.d include would take the http-block fallback for no reason.
func TestInsertNginxHTTPIncludeWithQuotedConfDAnchor(t *testing.T) {
quoted := strings.Replace(plainNginxConf,
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
` include "/usr/local/openresty/nginx/conf/conf.d/*.conf";`, 1)
got, err := insertNginxHTTPInclude(quoted)
if err != nil {
t.Fatal(err)
}
httpIdx := strings.Index(got, "conf/http.d/*.conf")
confDIdx := strings.Index(got, "conf/conf.d/*.conf")
if httpIdx < 0 || confDIdx < 0 || httpIdx > confDIdx {
t.Fatalf("quoted anchor not used; include misplaced:\n%s", got)
}
}
// A CRLF file must keep its own line endings after insertion.
func TestInsertNginxHTTPIncludeKeepsCRLFStyle(t *testing.T) {
crlf := strings.ReplaceAll(plainNginxConf, "\n", "\r\n")
got, err := insertNginxHTTPInclude(crlf)
if err != nil {
t.Fatal(err)
}
idx := strings.Index(got, nginxHTTPIncludeDirective)
if idx < 0 {
t.Fatal("include missing")
}
if got[idx-1] == '\n' || (idx >= 2 && got[idx-2:idx] != "\r\n" && got[idx-1] != ' ') {
// The inserted line must end with \r\n like the rest of the file.
}
end := idx + len(nginxHTTPIncludeDirective)
if end+2 > len(got) || got[end:end+2] != "\r\n" {
t.Fatalf("inserted line does not end with CRLF: %q", got[end:end+4])
}
}
+19 -124
View File
@@ -18,7 +18,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -165,51 +164,6 @@ func nginxModuleDynamicSupported(install model.AppInstall) bool {
fileOp.Stat(path.Join(buildPath, nginxModuleCatalogFile))
}
// nginxModuleStaticSupported reports whether the install can recompile its own
// OpenResty image, which is what a static module build needs. Versions before
// dynamic modules existed ship a compose file with a build section and the
// sources under build/; the oldest ones only reference a prebuilt image and
// cannot compile anything.
func nginxModuleStaticSupported(install model.AppInstall) bool {
if !files.NewFileOp().Stat(path.Join(install.GetPath(), nginxModuleBuildDir, "Dockerfile")) {
return false
}
envStr, err := coverEnvJsonToStr(install.Env)
if err != nil {
return false
}
project, err := dockerUtils.GetComposeProject(install.Name, install.GetPath(),
[]byte(install.DockerCompose), []byte(envStr), true)
if err != nil {
return false
}
for _, service := range project.AllServices() {
if service.Build != nil {
return true
}
}
return false
}
// defaultNginxModuleBuildMode picks the mode an install can actually perform.
//
// Module state written before build modes existed carries no buildMode at all.
// Rejecting it would fail loadNginxModules, and with it every module operation
// and the upgrade itself, so the value is inferred from what the install can
// do rather than assumed.
func defaultNginxModuleBuildMode(install model.AppInstall) string {
if nginxModuleDynamicSupported(install) {
return nginxModuleBuildDynamic
}
if nginxModuleStaticSupported(install) {
return nginxModuleBuildStatic
}
// Neither builder is available. Dynamic keeps the module inert instead of
// triggering an image rebuild that cannot succeed; the build itself still
// reports the missing capability.
return nginxModuleBuildDynamic
}
func syncNginxModuleBuilder(detailBuildDir, installBuildDir string) error {
sourcePath := path.Join(detailBuildDir, nginxModuleBuilderFile)
targetPath := path.Join(installBuildDir, nginxModuleBuilderFile)
@@ -700,56 +654,10 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N
return fmt.Errorf("validate combined dynamic module configuration: %w", err)
}
}
// Runtime directives live in http.d because load_module is main-context
// while directives such as "brotli on" are http-context. Both sets are
// written before nginx -t runs, so nginx only ever observes the final,
// consistent state; on failure both are rolled back together.
//
// The include that loads http.d is inserted on demand: only when a module
// actually needs runtime configuration. An install whose nginx.conf cannot
// be edited safely keeps the previous behaviour — the module loads but the
// runtime directives are skipped — rather than failing the operation.
httpConfigDir := nginxHTTPConfigDir(install)
desiredHTTP := desiredNginxModuleRuntimeConfigs(install, modules, target)
httpActive := nginxHTTPIncludePresent(install)
var nginxConfSnapshot []byte
if len(desiredHTTP) > 0 && !httpActive {
active, confSnapshot, includeErr := ensureNginxHTTPIncludeActive(install)
if includeErr != nil {
global.LOG.Warnf("cannot insert the http.d include into nginx.conf, skipping runtime directives: %v", includeErr)
desiredHTTP = nil
} else {
httpActive = active
nginxConfSnapshot = confSnapshot
}
}
var httpSnapshot nginxModuleConfigSnapshot
if httpActive {
if httpSnapshot, err = snapshotManagedNginxHTTPConfigs(httpConfigDir); err != nil {
return err
}
}
restore := func() {
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
if httpActive {
_ = applyManagedNginxHTTPConfigs(httpConfigDir, httpSnapshot)
}
if nginxConfSnapshot != nil {
_ = os.WriteFile(nginxMainConfigPath(install), nginxConfSnapshot, constant.FilePerm)
}
}
if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil {
restore()
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
return err
}
if httpActive {
if err = applyManagedNginxHTTPConfigs(httpConfigDir, desiredHTTP); err != nil {
restore()
return err
}
}
if !reload {
return nil
}
@@ -758,11 +666,11 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N
return nil
}
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
restore()
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
return err
}
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
restore()
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
return err
}
return nil
@@ -814,14 +722,6 @@ func applyManagedNginxModuleConfigs(configDir string, desired map[string][]byte)
return nil
}
// hasEnabledStaticNginxModules reports whether a full image rebuild is needed.
//
// Module state is the only input on purpose. RESTY_CONFIG_OPTIONS_MORE in .env
// is derived state: configureStaticNginxModules rewrites it from the modules
// below, and every build path calls that function before building. Treating a
// leftover value as a reason to rebuild would start a full recompile that
// configureStaticNginxModules has already reduced to an empty option list, so
// the rebuild could only reproduce the image it started from.
func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
for _, module := range modules {
normalizeNginxModule(&module)
@@ -832,6 +732,17 @@ func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
return false
}
func staticNginxBuildRequired(install model.AppInstall, modules []dto.NginxModule) bool {
if hasEnabledStaticNginxModules(modules) {
return true
}
envs, err := gotenv.Read(install.GetEnvPath())
if err != nil {
return false
}
return strings.TrimSpace(envs["RESTY_CONFIG_OPTIONS_MORE"]) != ""
}
func configureStaticNginxModules(install model.AppInstall, modules []dto.NginxModule, mirror string) error {
buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
var params, packages []string
@@ -896,17 +807,11 @@ func executeNginxModuleBuild(install model.AppInstall, reqModules []string, forc
if err != nil {
return err
}
// Only the module list decides this. A leftover RESTY_CONFIG_OPTIONS_MORE
// used to force the static path here, which meant a full image rebuild for
// an install that has no static module left to compile.
staticBuild := hasEnabledStaticNginxModules(modules)
if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) && !nginxModuleDynamicSupported(install) {
// The catalog and the builder have always shipped together, and an
// install missing the catalog fails to load its module state before
// this point, so this branch is a guard rather than a real path. Keep
// the error actionable instead of faking a build the state machine
// cannot record.
return buserr.New("ErrModuleBuildUnsupported")
staticBuild := staticNginxBuildRequired(install, modules)
if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) {
if !nginxModuleDynamicSupported(install) {
return errors.New("the installed OpenResty version does not support dynamic module builds")
}
}
if staticBuild {
return executeStaticNginxModuleBuild(install, modules, mirror, force, parentTask)
@@ -981,17 +886,7 @@ func loadNginxModulesWithCatalog(install model.AppInstall, catalogPath string) (
Builds: state.Builds, LastError: state.LastError,
})
}
// Catalog entries always declare a mode; state written before build modes
// existed does not. Fill the gap from the install's capabilities so an
// upgrade from such a version can still read its own module state.
fallbackMode := ""
for i := range modules {
if modules[i].BuildMode == "" {
if fallbackMode == "" {
fallbackMode = defaultNginxModuleBuildMode(install)
}
modules[i].BuildMode = fallbackMode
}
if err = validateNginxModuleBuildMode(modules[i]); err != nil {
return nil, err
}
-483
View File
@@ -1,483 +0,0 @@
package service
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
// nginxCompressibleTypes is shared by gzip_types and brotli_types so both
// encoders cover the same content. Already compressed formats (images other
// than SVG, woff/woff2, archives, media) are deliberately excluded:
// recompressing them costs CPU and usually grows the payload.
var nginxCompressibleTypes = []string{
"text/plain",
"text/css",
"text/xml",
"text/javascript",
"application/json",
"application/ld+json",
"application/javascript",
"application/x-javascript",
"application/xml",
"application/xhtml+xml",
"application/rss+xml",
"application/atom+xml",
"application/wasm",
"image/svg+xml",
"font/ttf",
"font/otf",
}
// nginxModuleRuntimeDefaults maps a module to the http-context directives that
// make it actually do something once loaded. Without these, enabling a module
// only emits load_module, leaving it loaded but inert.
//
// brotli_static is intentionally omitted: nginx does not verify that a .br
// file is newer than its source, so a stale artifact would be served
// indefinitely with no error.
var nginxModuleRuntimeDefaults = map[string][]nginxHTTPDirective{
"ngx_brotli": {
{Name: "brotli", Params: []string{"on"}},
// Brotli level 5 reaches roughly gzip level 9 ratio at a fraction of
// the cost. The nginx default of 6 is tuned for static assets and is
// too expensive for dynamic responses.
{Name: "brotli_comp_level", Params: []string{"5"}},
{Name: "brotli_min_length", Params: []string{"1k"}},
{Name: "brotli_types", Params: nginxCompressibleTypes},
},
}
// nginxModuleRuntimeLoadOrder keeps managed file names stable and ordered
// independently of the module load order used for load_module.
var nginxModuleRuntimeLoadOrder = map[string]int{
"ngx_brotli": 100,
}
func nginxModuleRuntimeOrder(name string) int {
if order, ok := nginxModuleRuntimeLoadOrder[name]; ok {
return order
}
return 900
}
// desiredNginxModuleRuntimeConfigs renders the managed http.d files for every
// enabled module that has a ready build and known runtime defaults.
//
// Values the user changed through the compression settings page are read back
// from the current managed file, so reconciling after an unrelated module
// change does not silently reset them to the defaults.
//
// A module the user already configured by hand in nginx.conf is skipped
// entirely. Emitting the same directive from an included file would make nginx
// reject the configuration as a duplicate, so their setup is left as the only
// definition.
func desiredNginxModuleRuntimeConfigs(install model.AppInstall, modules []dto.NginxModule, target dto.NginxModuleTarget) map[string][]byte {
desired := make(map[string][]byte)
for _, module := range modules {
normalizeNginxModule(&module)
// A custom module that happens to share a built-in name must not pick
// up the built-in's runtime defaults; the table is for catalog modules.
if module.Custom {
continue
}
directives, ok := nginxModuleRuntimeDefaults[module.Name]
if !ok || !module.Enable {
continue
}
if !nginxModuleRuntimeReady(module, target) {
continue
}
if nginxModuleConfiguredByUser(install, module.Name) {
continue
}
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(module.Name), module.Name)
current := readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
desired[fileName] = renderNginxHTTPConfig(mergeNginxRuntimeDirectives(directives, current))
}
return desired
}
// nginxModuleConfiguredByUser reports whether the user already manages any of
// the module's directives by hand.
//
// Users who enabled brotli before the panel managed it did so by editing
// nginx.conf or a file it includes. That definition has to keep winning: it is
// the one nginx has been running with, and adding a second one from http.d
// would break the configuration outright.
//
// Any brotli* directive counts, not just the primary one. A user who only
// tuned brotli_comp_level has still taken ownership of the block, and nginx
// allows the same directive at http and server scope, so a site-scoped value
// must suppress the managed one too.
func nginxModuleConfiguredByUser(install model.AppInstall, moduleName string) bool {
if _, ok := nginxModuleRuntimeDefaults[moduleName]; !ok {
return false
}
for _, filePath := range nginxModuleUserConfigPaths(install) {
content, err := os.ReadFile(filePath)
if err != nil {
continue
}
if nginxModuleUserDirectiveRe.MatchString(string(content)) {
return true
}
}
return false
}
// nginxModuleUserDirectiveRe matches any active (non-commented) brotli*
// directive at the start of a line, wherever it was written.
var nginxModuleUserDirectiveRe = regexp.MustCompile(`(?m)^[ \t]*brotli[a-z_]*[ \t]+[^;\n]*;`)
// nginxModuleUserConfigPaths lists the files that may carry a user's brotli
// configuration: the main config and the http-scope files it includes. The
// stream include is skipped on purpose — brotli is an http module and has no
// business there.
func nginxModuleUserConfigPaths(install model.AppInstall) []string {
return nginxModuleUserConfigPathsWithSiteDir(install, GetWebSiteRootDir())
}
// nginxModuleUserConfigPathsWithSiteDir is the testable core: the site conf
// directory is injected so unit tests do not need the settings database.
func nginxModuleUserConfigPathsWithSiteDir(install model.AppInstall, siteDir string) []string {
paths := []string{nginxMainConfigPath(install)}
paths = append(paths, globConfFiles(path.Join(siteDir, "conf.d"))...)
paths = append(paths, globConfFiles(path.Join(install.GetPath(), nginxModuleConfDir, "default"))...)
return paths
}
func globConfFiles(dir string) []string {
matches, err := filepath.Glob(path.Join(dir, "*.conf"))
if err != nil {
return nil
}
return matches
}
// nginxUserDirectivePattern matches a directive the user wrote in nginx.conf,
// capturing its indentation so a rewrite can keep the line's shape. Leading
// whitespace only, so a commented-out line never matches.
func nginxUserDirectivePattern(name string) *regexp.Regexp {
return regexp.MustCompile(`(?m)^([ \t]*)` + regexp.QuoteMeta(name) + `[ \t]+[^;\n]*;`)
}
// nginxConfigDefinesDirective reports whether a directive is set anywhere in
// the file, ignoring commented-out lines.
func nginxConfigDefinesDirective(content, name string) bool {
return nginxUserDirectivePattern(name).MatchString(content)
}
// mergeNginxRuntimeDirectives keeps the declared directive set and ordering
// while preferring values already present in the managed file.
func mergeNginxRuntimeDirectives(defaults []nginxHTTPDirective, current map[string][]string) []nginxHTTPDirective {
if len(current) == 0 {
return defaults
}
merged := make([]nginxHTTPDirective, 0, len(defaults))
for _, directive := range defaults {
if params, ok := current[directive.Name]; ok && len(params) > 0 {
directive.Params = params
}
merged = append(merged, directive)
}
return merged
}
// nginxBrotliModuleName is the catalog name of the brotli module.
const nginxBrotliModuleName = "ngx_brotli"
// getNginxBrotliParams reports the brotli settings currently in effect, and
// where they come from.
//
// Brotli is normally served from the managed http.d file instead of
// nginx.conf, so the directives can be removed together with the module. When
// the module is disabled the declared defaults are returned, which lets the
// settings page show what would be applied once it is enabled.
//
// If the user configured brotli anywhere nginx loads it from, those values
// are reported instead and ManagedExternally is set. Showing the managed
// defaults there would misrepresent what the server is actually running, and
// the panel must not write a second copy.
func getNginxBrotliParams() (*response.NginxBrotliRes, error) {
install, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return nil, err
}
managedExternally := nginxModuleConfiguredByUser(install, nginxBrotliModuleName)
var current map[string][]string
if managedExternally {
current = readNginxUserBrotliDirectives(install)
} else {
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
current = readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
}
res := &response.NginxBrotliRes{
ManagedExternally: managedExternally,
// Without the include, values the panel would write would never reach
// nginx, so they are reported as unavailable rather than shown as if
// they were in effect.
ManagedUnavailable: !managedExternally && !nginxHTTPIncludePresent(install),
}
for _, directive := range mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], current) {
res.Params = append(res.Params, response.NginxParam{Name: directive.Name, Params: directive.Params})
}
return res, nil
}
// readNginxUserBrotliDirectives collects the brotli directives the user wrote
// in any of the files nginx loads them from.
func readNginxUserBrotliDirectives(install model.AppInstall) map[string][]string {
directives := make(map[string][]string)
for _, filePath := range nginxModuleUserConfigPaths(install) {
content, err := os.ReadFile(filePath)
if err != nil {
continue
}
for _, name := range dto.BrotliKeys {
pattern := regexp.MustCompile(`(?m)^[ \t]*` + regexp.QuoteMeta(name) + `[ \t]+([^;\n]*);`)
if match := pattern.FindStringSubmatch(string(content)); match != nil {
if _, exists := directives[name]; !exists {
directives[name] = strings.Fields(strings.TrimSpace(match[1]))
}
}
}
}
return directives
}
// nginxBrotliValueRe whitelists what a brotli value may contain. The values
// are written into nginx.conf and the managed files verbatim; rejecting
// anything outside this set blocks both directive injection (`;`, newline,
// braces, quotes) and the `$` group-reference expansion of
// regexp.ReplaceAllString, which the in-place rewrite uses.
var nginxBrotliValueRe = regexp.MustCompile(`^[a-zA-Z0-9._+\-/:* ]+$`)
// validateNginxBrotliValues rejects any value outside the whitelist. The UI
// only sends on/off, numbers and sizes, but the endpoint is reachable
// directly.
func validateNginxBrotliValues(values map[string][]string) error {
for name, params := range values {
for _, param := range params {
if !nginxBrotliValueRe.MatchString(param) {
return buserr.WithDetail("ErrInvalidParams", fmt.Sprintf("invalid value for %s", name), nil)
}
}
}
return nil
}
// updateNginxBrotliParams persists brotli settings to the managed http.d file.
//
// Writing is refused unless the module is enabled and built: the directives
// would reference a module that is not loaded and nginx would fail to start.
func updateNginxBrotliParams(params []dto.NginxParam) error {
install, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
modules, err := loadNginxModules(install)
if err != nil {
return err
}
values := make(map[string][]string, len(params))
for _, param := range params {
values[param.Name] = param.Params
}
if err = validateNginxBrotliValues(values); err != nil {
return err
}
for i := range modules {
if modules[i].Name != nginxBrotliModuleName {
continue
}
if !modules[i].Enable {
return buserr.New("ErrBrotliDisabled")
}
// The user configured brotli in nginx.conf before the panel managed
// it. Update those lines in place: writing a managed file as well
// would define every directive twice and nginx would refuse to start.
if nginxModuleConfiguredByUser(install, nginxBrotliModuleName) {
return updateUserNginxBrotliParams(install, values)
}
// A managed write needs the include. Installations missing it are
// upgraded in place here; when nginx.conf cannot be edited safely the
// write is refused with an actionable error instead of writing values
// nginx would never load.
if !nginxHTTPIncludePresent(install) {
configPath := nginxMainConfigPath(install)
content, readErr := os.ReadFile(configPath)
if readErr != nil {
return readErr
}
updated, insErr := insertNginxHTTPInclude(string(content))
if insErr != nil {
return buserr.New("ErrBrotliUnsupported")
}
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
return err
}
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
return err
}
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
return err
}
}
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
configDir := nginxHTTPConfigDir(install)
snapshot, snapErr := snapshotManagedNginxHTTPConfigs(configDir)
if snapErr != nil {
return snapErr
}
merged := mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], values)
desired := map[string][]byte{fileName: renderNginxHTTPConfig(merged)}
for name, content := range snapshot {
if name != fileName {
desired[name] = content
}
}
if err = applyManagedNginxHTTPConfigs(configDir, desired); err != nil {
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
return err
}
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
return err
}
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
return err
}
// The directory is bind-mounted read-only and the include is a glob: a
// missing mount or an unrecognised include lets nginx -t pass while
// loading nothing. Read the effective configuration back instead of
// trusting the files we wrote.
if err = assertNginxBrotliActive(install.ContainerName); err != nil {
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
return buserr.New("ErrBrotliUnsupported")
}
return nil
}
return buserr.New("ErrBrotliDisabled")
}
// assertNginxBrotliActive confirms the managed brotli directives are in the
// running server's effective configuration. It is the only check that catches
// a bind mount that never reached the container or an include variant the
// detection missed — both pass nginx -t and reload silently.
func assertNginxBrotliActive(containerName string) error {
out, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
"docker", "exec", "-i", containerName, "nginx", "-T")
if err != nil {
return err
}
if !nginxModuleUserDirectiveRe.MatchString(out) {
return errors.New("brotli directives are not in the effective nginx configuration")
}
return nil
}
// updateUserNginxBrotliParams rewrites the brotli directives the user wrote
// into nginx.conf, in place.
//
// Only the values change: each directive keeps its original line and
// indentation, and every other line is untouched, so a hand-maintained config
// survives an edit from the settings page. Directives the user did not write
// are not introduced, since the panel cannot know where they intended them.
//
// A managed file can still be on disk when the panel managed brotli before
// the user wrote their own directives. Leaving it behind would make every
// directive duplicate once the user's config is touched, so it is removed
// first and rolled back together with the config on a failed nginx -t.
func updateUserNginxBrotliParams(install model.AppInstall, values map[string][]string) error {
configPath := nginxMainConfigPath(install)
content, err := os.ReadFile(configPath)
if err != nil {
return err
}
configDir := nginxHTTPConfigDir(install)
httpSnapshot, snapErr := snapshotManagedNginxHTTPConfigs(configDir)
if snapErr != nil {
return snapErr
}
managedFile := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
if _, stale := httpSnapshot[managedFile]; stale {
remaining := make(map[string][]byte, len(httpSnapshot))
for name, fileContent := range httpSnapshot {
if name != managedFile {
remaining[name] = fileContent
}
}
if err = applyManagedNginxHTTPConfigs(configDir, remaining); err != nil {
return err
}
}
restore := func() {
_ = writeNginxFileAtomic(configPath, content)
_ = applyManagedNginxHTTPConfigs(configDir, httpSnapshot)
}
updated := string(content)
for _, name := range dto.BrotliKeys {
params, ok := values[name]
if !ok || len(params) == 0 {
continue
}
pattern := nginxUserDirectivePattern(name)
if !pattern.MatchString(updated) {
continue
}
replacement := "${1}" + name + " " + strings.Join(params, " ") + ";"
updated = pattern.ReplaceAllString(updated, replacement)
}
if updated == string(content) {
return nil
}
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
restore()
return err
}
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
restore()
return err
}
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
restore()
return err
}
return nil
}
// nginxModuleRuntimeReady reports whether the module is actually usable.
//
// Dynamic modules need a ready build for the current target, otherwise the
// .so is missing and nginx would reject the directives. Static modules are
// compiled into the binary and carry no artifacts, so an enabled static
// module is considered ready. This rests on a data premise: the catalog only
// declares a module static when the image ships it. Checking for a build
// record instead would be wrong here — reconcile runs inside the static build
// flow, before the record for the build in progress exists, and would drop
// the runtime configuration of the module that was just compiled in.
func nginxModuleRuntimeReady(module dto.NginxModule, target dto.NginxModuleTarget) bool {
if module.BuildMode == nginxModuleBuildStatic {
return true
}
build := findCurrentNginxModuleBuild(module, target)
if build == nil || build.Status != nginxModuleStatusReady {
build = findLatestNginxModuleBuild(module, target)
}
return build != nil && build.Status == nginxModuleStatusReady
}
@@ -1,231 +0,0 @@
package service
import (
"os"
"path"
"strings"
"testing"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
)
const userBrotliConf = `user root;
worker_processes auto;
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
events { use epoll; }
http {
include mime.types;
default_type application/octet-stream;
gzip on;
gzip_comp_level 5;
# enabled by hand, long before the panel managed it
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json;
include /usr/local/openresty/nginx/conf/http.d/*.conf;
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
}
`
func TestNginxConfigDefinesDirective(t *testing.T) {
cases := []struct {
name string
content string
want bool
}{
{"directive present", userBrotliConf, true},
{"absent", strings.Replace(userBrotliConf, " brotli on;\n", "", 1), false},
{
name: "commented out does not count",
content: strings.Replace(userBrotliConf, " brotli on;", " # brotli on;", 1),
want: false,
},
{
name: "a longer directive name is not a match",
content: "http {\n brotli_comp_level 6;\n}\n",
want: false,
},
{
name: "indentation does not matter",
content: "http {\n\t\tbrotli on;\n}\n",
want: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := nginxConfigDefinesDirective(tc.content, "brotli"); got != tc.want {
t.Fatalf("expected %v, got %v", tc.want, got)
}
})
}
}
// The detection must catch any brotli* directive, in any file nginx loads it
// from, not only the primary directive in nginx.conf.
func TestNginxModuleUserDirectiveRe(t *testing.T) {
cases := []struct {
name string
content string
want bool
}{
{"primary directive", "http {\n brotli on;\n}", true},
{"a tuning directive alone", "server {\n brotli_comp_level 11;\n}", true},
{"another variant", "http {\n brotli_types text/plain;\n}", true},
{"server scope in a site file", "server {\n listen 80;\n brotli on;\n}", true},
{"commented out does not count", "http {\n # brotli on;\n}", false},
{"indented comment does not count", "http {\n # brotli_comp_level 6;\n}", false},
{"no brotli at all", "http {\n gzip on;\n}", false},
{"a similarly named directive is not a match", "http {\n gzip on;\n}", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := nginxModuleUserDirectiveRe.MatchString(tc.content); got != tc.want {
t.Fatalf("expected %v, got %v", tc.want, got)
}
})
}
}
// The panel must not emit a managed file for a module the user already
// configured: nginx rejects the same directive defined twice.
func TestUserConfiguredBrotliSuppressesManagedFile(t *testing.T) {
if !nginxModuleUserDirectiveRe.MatchString(userBrotliConf) {
t.Fatal("a hand-written brotli config must be detected")
}
clean := strings.Replace(userBrotliConf, " brotli on;\n", "", 1)
clean = strings.Replace(clean, " brotli_comp_level 6;\n", "", 1)
clean = strings.Replace(clean, " brotli_types text/plain text/css application/json;\n", "", 1)
if nginxModuleUserDirectiveRe.MatchString(clean) {
t.Fatal("a config without brotli must not be treated as user-managed")
}
}
func TestRewriteUserBrotliDirectivesInPlace(t *testing.T) {
// Mirrors updateUserNginxBrotliParams without touching the filesystem.
rewrite := func(content string, values map[string][]string) string {
updated := content
for _, name := range []string{"brotli", "brotli_comp_level", "brotli_min_length", "brotli_types"} {
params, ok := values[name]
if !ok || len(params) == 0 {
continue
}
pattern := nginxUserDirectivePattern(name)
if !pattern.MatchString(updated) {
continue
}
updated = pattern.ReplaceAllString(updated, "${1}"+name+" "+strings.Join(params, " ")+";")
}
return updated
}
got := rewrite(userBrotliConf, map[string][]string{
"brotli": {"off"},
"brotli_comp_level": {"4"},
// brotli_min_length is absent from the user's config and must not be
// introduced: the panel cannot know where they would want it.
"brotli_min_length": {"2k"},
})
if !strings.Contains(got, " brotli off;") {
t.Errorf("value was not updated:\n%s", got)
}
if !strings.Contains(got, " brotli_comp_level 4;") {
t.Errorf("comp level was not updated:\n%s", got)
}
if strings.Contains(got, "brotli_min_length") {
t.Error("a directive the user never wrote must not be added")
}
// Everything else survives, including the comment the parser would drop.
for _, keep := range []string{
"# enabled by hand, long before the panel managed it",
" gzip on;",
" gzip_comp_level 5;",
" brotli_types text/plain text/css application/json;",
"include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
"worker_processes auto;",
} {
if !strings.Contains(got, keep) {
t.Errorf("unrelated line was altered or lost: %s", keep)
}
}
if strings.Count(got, "brotli on;")+strings.Count(got, "brotli off;") != 1 {
t.Error("the directive must remain defined exactly once")
}
}
func TestRewriteUserBrotliPreservesIndentation(t *testing.T) {
content := "http {\n\t\tbrotli on;\n}\n"
pattern := nginxUserDirectivePattern("brotli")
got := pattern.ReplaceAllString(content, "${1}brotli off;")
if !strings.Contains(got, "\t\tbrotli off;") {
t.Errorf("original indentation was not preserved: %q", got)
}
}
// Detection must cover the default/ directory, which is included at http
// scope like conf.d but lives under the install directory, not the site root.
func TestNginxModuleUserConfigPathsCoversDefaultDir(t *testing.T) {
siteDir := t.TempDir()
installRoot := t.TempDir()
installDir := path.Join(installRoot, "openresty", "openresty")
defaultDir := path.Join(installDir, "conf", "default")
if err := os.MkdirAll(defaultDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path.Join(defaultDir, "00.default.conf"), []byte("server {}\n"), 0o644); err != nil {
t.Fatal(err)
}
global.Dir.AppInstallDir = installRoot
install := model.AppInstall{Name: "openresty"}
install.App.Key = constant.AppOpenresty
paths := nginxModuleUserConfigPathsWithSiteDir(install, siteDir)
foundMain, foundDefault := false, false
for _, p := range paths {
if strings.HasSuffix(p, path.Join("conf", "nginx.conf")) {
foundMain = true
}
if strings.HasSuffix(p, path.Join("conf", "default", "00.default.conf")) {
foundDefault = true
}
}
if !foundMain {
t.Error("nginx.conf must be scanned")
}
if !foundDefault {
t.Error("conf/default must be scanned: it is included at http scope")
}
}
// Values outside the whitelist must never reach nginx.conf or the managed
// files: they could inject directives or trigger regexp group expansion.
func TestValidateNginxBrotliValues(t *testing.T) {
valid := map[string][]string{
"brotli": {"on"},
"brotli_comp_level": {"11"},
"brotli_min_length": {"1k"},
"brotli_types": {"text/plain", "application/json", "application/ld+json"},
}
if err := validateNginxBrotliValues(valid); err != nil {
t.Fatalf("legitimate values rejected: %v", err)
}
for name, bad := range map[string]string{
"directive injection": "off; gzip on",
"newline injection": "off\nbrotli off;",
"group reference": "$1",
"brace": "${1}",
"quote": `"on"`,
} {
if err := validateNginxBrotliValues(map[string][]string{"brotli": {bad}}); err == nil {
t.Errorf("%s: %q must be rejected", name, bad)
}
}
}
@@ -1,83 +0,0 @@
package service
import (
"testing"
"github.com/1Panel-dev/1Panel/agent/app/dto"
)
func TestHasEnabledStaticNginxModules(t *testing.T) {
cases := []struct {
name string
modules []dto.NginxModule
want bool
}{
{
name: "an enabled static module requires a rebuild",
modules: []dto.NginxModule{
{Name: "custom", Enable: true, BuildMode: nginxModuleBuildStatic},
},
want: true,
},
{
name: "a disabled static module does not",
modules: []dto.NginxModule{
{Name: "custom", Enable: false, BuildMode: nginxModuleBuildStatic},
},
want: false,
},
{
name: "dynamic modules never require a rebuild",
modules: []dto.NginxModule{
{Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic},
},
want: false,
},
{
name: "no modules at all",
modules: nil,
want: false,
},
{
name: "one enabled static module among dynamic ones is enough",
modules: []dto.NginxModule{
{Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic},
{Name: "custom", Enable: true, BuildMode: nginxModuleBuildStatic},
},
want: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := hasEnabledStaticNginxModules(tc.modules); got != tc.want {
t.Fatalf("expected %v, got %v", tc.want, got)
}
})
}
}
// A stale RESTY_CONFIG_OPTIONS_MORE used to force the full-rebuild path even
// with no static module enabled. configureStaticNginxModules derives that value
// from the module list and runs before every build, so the rebuild it triggered
// could only ever reproduce the current image. Module state is now the only
// input; this test pins that down.
func TestStaticRebuildIgnoresLeftoverBuildOptions(t *testing.T) {
modules := []dto.NginxModule{
{Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic},
}
if hasEnabledStaticNginxModules(modules) {
t.Fatal("dynamic-only modules must not select the static build path")
}
}
// normalizeNginxModule is applied to a copy, so callers keep their entities.
func TestHasEnabledStaticNginxModulesDoesNotMutateInput(t *testing.T) {
modules := []dto.NginxModule{
{Name: "custom", Enable: true, BuildMode: nginxModuleBuildStatic, Packages: []string{"", "libfoo", ""}},
}
_ = hasEnabledStaticNginxModules(modules)
if len(modules[0].Packages) != 3 {
t.Fatalf("input was normalized in place: %v", modules[0].Packages)
}
}
+22 -47
View File
@@ -640,12 +640,6 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
runtime.Version = req.Version
return runtimeRepo.Save(runtime)
}
appDetail, err := getRuntimeUpdateAppDetail(runtime, req)
if err != nil {
return err
}
versionChanged := appDetail.ID != runtime.AppDetailID || appDetail.Version != runtime.Version
req.Version = appDetail.Version
oldImage := runtime.Image
oldEnv := runtime.Env
var hostPorts []string
@@ -665,6 +659,23 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
}
}
appDetail, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
if err != nil {
return err
}
app, err := appRepo.GetFirst(repo.WithByID(appDetail.AppId))
if err != nil {
return err
}
fileOp := files.NewFileOp()
appVersionDir := path.Join(global.Dir.AppResourceDir, app.Resource, app.Key, appDetail.Version)
if !fileOp.Stat(appVersionDir) || appDetail.Update {
if err := downloadApp(app, appDetail, nil, nil); err != nil {
return err
}
_ = fileOp.Rename(path.Join(runtime.GetPath(), "run.sh"), path.Join(runtime.GetPath(), "run.sh.bak"))
_ = fileOp.CopyFile(path.Join(appVersionDir, "run.sh"), runtime.GetPath())
}
}
if containerName, ok := req.Params["CONTAINER_NAME"]; ok && containerName != getRuntimeEnv(runtime.Env, "CONTAINER_NAME") {
@@ -674,36 +685,6 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
runtime.ContainerName = containerName.(string)
}
app, err := appRepo.GetFirst(repo.WithByID(appDetail.AppId))
if err != nil {
return err
}
fileOp := files.NewFileOp()
appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version)
refreshTemplate := !fileOp.Stat(appVersionDir) || appDetail.Update
if err = downloadApp(app, appDetail, nil, nil); err != nil {
return err
}
if versionChanged {
if err = updateRuntimeVersionFiles(runtime, appVersionDir); err != nil {
return err
}
} else if refreshTemplate && runtime.Type != constant.RuntimePHP {
if err = fileOp.CopyFile(filepath.Join(appVersionDir, "run.sh"), runtime.GetPath()); err != nil {
return err
}
}
if runtime.Type == constant.RuntimePHP {
composeContent, err := fileOp.GetContent(runtime.GetComposePath())
if err != nil {
return err
}
req.Environments, err = getDockerComposeEnvironments(composeContent)
if err != nil {
return err
}
}
projectDir := path.Join(global.Dir.RuntimeDir, runtime.Type, runtime.Name)
create := request.RuntimeCreate{
Image: req.Image,
@@ -721,21 +702,19 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
ExtraHosts: req.ExtraHosts,
},
}
composeContent, envContent, forms, err := handleParams(create, projectDir)
composeContent, envContent, _, err := handleParams(create, projectDir)
if err != nil {
return err
}
runtime.Remark = req.Remark
runtime.AppDetailID = appDetail.ID
runtime.Version = appDetail.Version
runtime.Env = string(envContent)
runtime.DockerCompose = string(composeContent)
switch runtime.Type {
case constant.RuntimePHP:
runtime.Image = req.Image
runtime.Params = string(forms)
runtime.Status = constant.StatusBuilding
_ = runtimeRepo.Save(runtime)
client, err := docker.NewClient()
if err != nil {
return err
@@ -745,18 +724,14 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
if err != nil {
return err
}
if err = runtimeRepo.Save(runtime); err != nil {
return err
}
go buildRuntime(runtime, imageID, oldEnv, req.Rebuild || versionChanged)
go buildRuntime(runtime, imageID, oldEnv, req.Rebuild)
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
runtime.Version = req.Version
runtime.CodeDir = req.CodeDir
runtime.Port = strings.Join(hostPorts, ",")
runtime.Status = constant.StatusReCreating
runtime.ContainerName = req.Params["CONTAINER_NAME"].(string)
if err = runtimeRepo.Save(runtime); err != nil {
return err
}
_ = runtimeRepo.Save(runtime)
go reCreateRuntime(runtime)
}
return nil
-106
View File
@@ -1,106 +0,0 @@
package service
import (
"path/filepath"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"gopkg.in/yaml.v3"
)
func getRuntimeUpdateAppDetail(runtime *model.Runtime, req request.RuntimeUpdate) (model.AppDetail, error) {
current, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
if err != nil {
return model.AppDetail{}, err
}
if current.ID == 0 {
return model.AppDetail{}, buserr.New("ErrRecordNotFound")
}
opts := []repo.DBOption{appDetailRepo.WithAppId(current.AppId)}
if req.AppDetailID != 0 {
opts = append(opts, repo.WithByID(req.AppDetailID))
} else if req.Version != "" {
opts = append(opts, appDetailRepo.WithVersion(req.Version))
} else {
return current, nil
}
detail, err := appDetailRepo.GetFirst(opts...)
if err != nil {
return model.AppDetail{}, err
}
if detail.ID == 0 || (req.Version != "" && detail.Version != req.Version) {
return model.AppDetail{}, buserr.New("ErrInvalidParams")
}
return detail, nil
}
func updateRuntimeVersionFiles(runtime *model.Runtime, appVersionDir string) error {
fileOp := files.NewFileOp()
template, err := fileOp.GetContent(filepath.Join(appVersionDir, "docker-compose.yml"))
if err != nil {
return err
}
current, err := fileOp.GetContent(runtime.GetComposePath())
if err != nil {
return err
}
composeContent, err := updateRuntimeImageConfig(current, template)
if err != nil {
return err
}
if runtime.Type == constant.RuntimePHP {
if err = fileOp.CopyDir(filepath.Join(appVersionDir, "build"), runtime.GetPath()); err != nil {
return err
}
if err = fileOp.CopyFile(filepath.Join(appVersionDir, "data.yml"), runtime.GetPath()); err != nil {
return err
}
} else {
if err = fileOp.CopyFile(filepath.Join(appVersionDir, "run.sh"), runtime.GetPath()); err != nil {
return err
}
}
return fileOp.SaveFile(runtime.GetComposePath(), string(composeContent), constant.FilePerm)
}
func updateRuntimeImageConfig(current, template []byte) ([]byte, error) {
var currentCompose, templateCompose map[string]interface{}
if err := yaml.Unmarshal(current, &currentCompose); err != nil {
return nil, err
}
if err := yaml.Unmarshal(template, &templateCompose); err != nil {
return nil, err
}
currentServices, ok := currentCompose["services"].(map[string]interface{})
if !ok || len(currentServices) != 1 {
return nil, buserr.New("ErrFileParse")
}
templateServices, ok := templateCompose["services"].(map[string]interface{})
if !ok || len(templateServices) != 1 {
return nil, buserr.New("ErrFileParse")
}
// Keep container settings and mounts; only the image and build definition belong to the version.
for _, currentService := range currentServices {
service, ok := currentService.(map[string]interface{})
if !ok {
return nil, buserr.New("ErrFileParse")
}
for _, templateService := range templateServices {
target, ok := templateService.(map[string]interface{})
if !ok || (target["image"] == nil && target["build"] == nil) {
return nil, buserr.New("ErrFileParse")
}
for _, key := range []string{"image", "build"} {
delete(service, key)
if value, exists := target[key]; exists {
service[key] = value
}
}
}
}
return yaml.Marshal(currentCompose)
}
+12 -40
View File
@@ -838,49 +838,21 @@ func restartRuntime(runtime *model.Runtime) (err error) {
}
func getDockerComposeEnvironments(yml []byte) ([]request.Environment, error) {
var project struct {
Services yaml.Node `yaml:"services"`
}
if err := yaml.Unmarshal(yml, &project); err != nil {
var (
composeProject docker.ComposeProject
err error
)
err = yaml.Unmarshal(yml, &composeProject)
if err != nil {
return nil, err
}
services := &project.Services
if services.Kind == yaml.AliasNode {
services = services.Alias
}
if services.Kind != 0 && services.Kind != yaml.MappingNode {
return nil, fmt.Errorf("unsupported services format")
}
var res []request.Environment
// Keep the file order for both services and environment entries.
for i := 1; i < len(services.Content); i += 2 {
var service struct {
Environment yaml.Node `yaml:"environment"`
}
if err := services.Content[i].Decode(&service); err != nil {
return nil, err
}
environment := &service.Environment
if environment.Kind == yaml.AliasNode {
environment = environment.Alias
}
switch environment.Kind {
case yaml.MappingNode:
for j := 0; j < len(environment.Content); j += 2 {
res = append(res, request.Environment{Key: environment.Content[j].Value, Value: environment.Content[j+1].Value})
}
case yaml.SequenceNode:
for _, item := range environment.Content {
var entry string
if err := item.Decode(&entry); err != nil {
return nil, err
}
key, value, _ := strings.Cut(entry, "=")
res = append(res, request.Environment{Key: key, Value: value})
}
case 0:
default:
return nil, fmt.Errorf("unsupported environment format")
for _, service := range composeProject.Services {
for key, value := range service.Environment.Variables {
res = append(res, request.Environment{
Key: key,
Value: value,
})
}
}
return res, nil
+17 -1
View File
@@ -18,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
"github.com/jinzhu/copier"
@@ -124,7 +125,22 @@ func (u *SettingService) GetWebsiteDir() string {
}
func (u *SettingService) Update(key, value string) error {
return settingRepo.UpdateOrCreate(key, value)
oldValue := constant.FirewallPortWhiteListValue
if key == constant.FirewallPortWhiteList {
if _, err := firewall.ParsePortWhitelist(value); err != nil {
return err
}
if val, err := settingRepo.GetValueByKey(key); err == nil {
oldValue = val
}
}
if err := settingRepo.UpdateOrCreate(key, value); err != nil {
return err
}
if key == constant.FirewallPortWhiteList {
return SyncFirewallPortWhitelistAfterUpdate(oldValue)
}
return nil
}
func (u *SettingService) UpdateTerminalAI(req dto.TerminalAIInfo) error {
+42 -73
View File
@@ -4,7 +4,6 @@ import (
"bufio"
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
@@ -23,7 +22,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/copier"
csvexport "github.com/1Panel-dev/1Panel/agent/utils/csv_export"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/geo"
"github.com/gin-gonic/gin"
@@ -42,7 +40,6 @@ import (
const sshPath = "/etc/ssh/sshd_config"
const defaultSSHPort = "22"
const sshManagedMarker = "# config by 1panel"
const defaultSSHLogDir = "/var/log"
type SSHService struct{}
@@ -219,21 +216,10 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
return err
}
oldPortValue := strings.Join(loadSSHPortValues(directives), ",")
if req.Key == "Port" {
if err := checkSSHPortAvailability(splitSSHPorts(oldPortValue), splitSSHPorts(req.NewValue)); err != nil {
return err
}
}
if err := updateSSHDirectiveValue(req.Key, req.NewValue, directives); err != nil {
return err
}
if req.Key == "Port" {
if err := updateSystemAccessPortWhitelist(context.Background(), firewall.PortWhitelistTypeSSH, splitSSHPorts(req.NewValue)); err != nil {
if restoreErr := rewriteSSHManagedDirectives(sshPath, "Port", buildSSHDirectiveLines("Port", oldPortValue)); restoreErr != nil {
return fmt.Errorf("save SSH whitelist: %w; restore SSH configuration: %v", err, restoreErr)
}
return err
}
handleSSHPortUpdate(oldPortValue, req.NewValue)
}
@@ -333,6 +319,18 @@ func handleSSHPortUpdate(oldValue, newValue string) {
}
}
removedPorts, err := parseSSHPortsToInts(diffSSHPorts(oldPorts, newPorts))
if err != nil {
global.LOG.Errorf("parse removed ssh ports failed, err: %v", err)
} else {
addedPorts, err := parseSSHPortsToInts(diffSSHPorts(newPorts, oldPorts))
if err != nil {
global.LOG.Errorf("parse added ssh ports failed, err: %v", err)
} else if err := OperateFirewallPort(removedPorts, addedPorts); err != nil {
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", oldValue, newValue, err)
}
}
primaryPort, err := loadPrimarySSHPort(newValue)
if err != nil {
global.LOG.Errorf("load primary ssh port from %s failed, err: %v", newValue, err)
@@ -372,24 +370,6 @@ func diffSSHPorts(left, right []string) []string {
return diff
}
func checkSSHPortAvailability(oldPorts, newPorts []string) error {
for _, port := range diffSSHPorts(newPorts, oldPorts) {
value, err := strconv.Atoi(port)
if err != nil || value < 1 || value > 65535 {
return fmt.Errorf("invalid SSH port %q", port)
}
if common.ScanPort(value) {
return buserr.WithDetail("ErrPortInUsed", value, nil)
}
listener, err := net.Listen("tcp4", ":"+strconv.Itoa(value))
if err != nil {
return buserr.WithDetail("ErrPortInUsed", value, nil)
}
_ = listener.Close()
}
return nil
}
func loadPrimarySSHPort(value string) (int, error) {
ports := splitSSHPorts(value)
if len(ports) == 0 {
@@ -398,6 +378,18 @@ func loadPrimarySSHPort(value string) (int, error) {
return strconv.Atoi(ports[0])
}
func parseSSHPortsToInts(ports []string) ([]int, error) {
var values []int
for _, port := range ports {
value, err := strconv.Atoi(port)
if err != nil {
return nil, err
}
values = append(values, value)
}
return values, nil
}
func runWithOptionalSudo(sudo, name string, args ...string) (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
if sudo != "" {
@@ -676,11 +668,13 @@ func isSSHLogFileName(name string) bool {
return false
}
func listSSHLogFiles(baseDir string) ([]sshFileItem, error) {
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) {
var fileList []sshFileItem
var data []dto.SSHHistory
baseDir := "/var/log"
fileItems, err := os.ReadDir(baseDir)
if err != nil {
return nil, err
return 0, data, err
}
for _, item := range fileItems {
if item.IsDir() || !isSSHLogFileName(item.Name()) {
@@ -688,7 +682,7 @@ func listSSHLogFiles(baseDir string) ([]sshFileItem, error) {
}
info, err := item.Info()
if err != nil {
return nil, err
return 0, data, err
}
if !info.Mode().IsRegular() {
continue
@@ -701,15 +695,7 @@ func listSSHLogFiles(baseDir string) ([]sshFileItem, error) {
}
fileList = append(fileList, sshFileItem{Name: itemPath, Year: info.ModTime().Year()})
}
return sortFileList(fileList), nil
}
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) {
var data []dto.SSHHistory
fileList, err := listSSHLogFiles(defaultSSHLogDir)
if err != nil {
return 0, data, err
}
fileList = sortFileList(fileList)
filter := ""
if len(req.Info) != 0 {
@@ -756,7 +742,7 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
}
func (u *SSHService) CleanLog() error {
return cleanSSHLogFiles(defaultSSHLogDir)
return cleanSSHLogFiles("/var/log")
}
func cleanSSHLogFiles(baseDir string) error {
@@ -1304,11 +1290,20 @@ func loadSSHData(
if err != nil {
return datas, 0, 0
}
histories, err := loadSSHHistoriesFromFile(filePath, status, filter, startTime, endTime, currentYear, nyc)
lines, err := loadSSHLogLines(filePath)
if err != nil {
return datas, 0, 0
}
for _, itemData := range histories {
items := collectSSHLogItems(lines, filter, status)
for i := len(items) - 1; i >= 0; i-- {
itemData := items[i].History
if !matchSSHLogStatus(status, itemData.Status) || !checkIsStandard(itemData) {
continue
}
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
if !isSSHLogWithinTimeRange(itemData.Date, startTime, endTime) {
continue
}
if successCount+failedCount >= showCountFrom && (showCountTo == -1 || successCount+failedCount < showCountTo) {
itemData.Area, _ = geo.GetIPLocation(getLoc, itemData.Address, common.GetLang(ctx))
datas = append(datas, itemData)
@@ -1322,32 +1317,6 @@ func loadSSHData(
return datas, successCount, failedCount
}
func loadSSHHistoriesFromFile(
filePath, status, filter string,
startTime, endTime time.Time,
currentYear int,
location *time.Location,
) ([]dto.SSHHistory, error) {
lines, err := loadSSHLogLines(filePath)
if err != nil {
return nil, err
}
items := collectSSHLogItems(lines, filter, status)
histories := make([]dto.SSHHistory, 0, len(items))
for i := len(items) - 1; i >= 0; i-- {
itemData := items[i].History
if !matchSSHLogStatus(status, itemData.Status) || !checkIsStandard(itemData) {
continue
}
itemData.Date = loadDate(currentYear, itemData.DateStr, location)
if !isSSHLogWithinTimeRange(itemData.Date, startTime, endTime) {
continue
}
histories = append(histories, itemData)
}
return histories, nil
}
func isSSHLogWithinTimeRange(itemTime, startTime, endTime time.Time) bool {
if startTime.IsZero() || endTime.IsZero() {
return true
+18 -26
View File
@@ -4,38 +4,23 @@ import (
"encoding/json"
"strings"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
)
const (
vllmAppKeyForUpgrade = "vllm"
vllmGB10VersionPrefix = "nvidia-gb10-dspark-"
vllmImageEnvKey = "IMAGE"
vllmImageTypeNvidia = "nvidia"
vllmImageTypeIntel = "intel"
vllmImageTypeAscend = "ascend"
vllmAppKeyForUpgrade = "vllm"
vllmImageEnvKey = "IMAGE"
vllmImageTypeNvidia = "nvidia"
vllmImageTypeIntel = "intel"
vllmImageTypeAscend = "ascend"
vllmImageTypeGB10 = "nvidia-gb10-dspark"
)
func isVllmProOnlyVersion(appKey, version string) bool {
return strings.EqualFold(strings.TrimSpace(appKey), vllmAppKeyForUpgrade) &&
strings.HasPrefix(strings.ToLower(strings.TrimSpace(version)), vllmGB10VersionPrefix)
}
func canAccessVllmVersion(appKey, version string) bool {
return !isVllmProOnlyVersion(appKey, version) || xpack.MultiNodeProvider.IsXpack()
}
func checkVllmVersionAccess(appKey, version string) error {
if !canAccessVllmVersion(appKey, version) {
return buserr.New("ErrVllmGB10ProOnly")
}
return nil
}
func resolveVllmVersionFamily(version, image string) string {
normalizedVersion := strings.ToLower(strings.TrimSpace(version))
if strings.HasPrefix(normalizedVersion, vllmImageTypeGB10+"-") {
return vllmImageTypeGB10
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
return vllmImageTypeIntel
}
@@ -46,6 +31,9 @@ func resolveVllmVersionFamily(version, image string) string {
return vllmImageTypeNvidia
}
normalizedImage := strings.ToLower(strings.TrimSpace(image))
if strings.Contains(normalizedImage, "vllm-gb10-dspark") {
return vllmImageTypeGB10
}
if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") {
return vllmImageTypeIntel
}
@@ -58,7 +46,7 @@ func resolveVllmVersionFamily(version, image string) string {
func trimVllmVersionFamily(version string) string {
trimmed := strings.TrimSpace(version)
normalized := strings.ToLower(trimmed)
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
for _, family := range []string{vllmImageTypeGB10, vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
prefix := family + "-"
if strings.HasPrefix(normalized, prefix) {
return strings.TrimSpace(trimmed[len(prefix):])
@@ -70,6 +58,9 @@ func trimVllmVersionFamily(version string) string {
func buildDefaultVllmImageByVersion(version string) string {
tag := trimVllmVersionFamily(version)
family := resolveVllmVersionFamily(version, "")
if family == vllmImageTypeGB10 {
return "1panel/vllm-gb10-dspark:" + tag
}
if family == vllmImageTypeIntel {
return "intel/llm-scaler-vllm:" + tag
}
@@ -90,7 +81,8 @@ func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage str
func hasVllmVersionFamilyPrefix(version string) bool {
normalized := strings.ToLower(strings.TrimSpace(version))
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
return strings.HasPrefix(normalized, vllmImageTypeGB10+"-") ||
strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
strings.HasPrefix(normalized, vllmImageTypeIntel+"-") ||
strings.HasPrefix(normalized, vllmImageTypeAscend+"-")
}
+31 -38
View File
@@ -265,19 +265,6 @@ func (w WebsiteService) GetWebsites() ([]response.WebsiteDTO, error) {
return websiteDTOs, nil
}
func newWebsiteCreateHTTPSOp(sslID uint) request.WebsiteHTTPSOp {
return request.WebsiteHTTPSOp{
Enable: true,
WebsiteSSLID: sslID,
Type: constant.SSLExisted,
HttpConfig: constant.HTTPToHTTPS,
SSLProtocol: []string{"TLSv1.3", "TLSv1.2"},
Algorithm: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED",
Hsts: true,
HstsIncludeSubDomains: true,
}
}
func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) {
alias := create.Alias
if alias == "default" {
@@ -326,7 +313,6 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
appInstall *model.AppInstall
runtime *model.Runtime
primaryDomain string
initialSSL *websiteInitialSSL
)
if website.Type == constant.Stream {
if create.StreamConfig.StreamPorts == "" {
@@ -353,29 +339,6 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
}
website.PrimaryDomain = primaryDomain
website.Protocol = constant.ProtocolHTTP
for _, domain := range domains {
if domain.SSL {
create.EnableSSL = true
break
}
}
if create.EnableSSL {
if create.WebsiteSSLID == 0 {
return buserr.New("ErrSSLValid")
}
websiteSSL, sslErr := websiteSSLRepo.GetFirst(repo.WithByID(create.WebsiteSSLID))
if sslErr != nil {
return sslErr
}
if websiteSSL.Pem == "" || websiteSSL.PrivateKey == "" {
return buserr.New("ErrSSLValid")
}
sslReq := newWebsiteCreateHTTPSOp(websiteSSL.ID)
website.Protocol = constant.ProtocolHTTPS
website.WebsiteSSLID = websiteSSL.ID
website.HttpConfig = sslReq.HttpConfig
initialSSL = &websiteInitialSSL{certificate: *websiteSSL, request: sslReq}
}
}
createTask, err := task.NewTaskWithOps(website.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID, 0)
@@ -527,7 +490,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
}
configNginx := func(t *task.Task) error {
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL); err != nil {
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig); err != nil {
return err
}
if create.Type == constant.Static && create.TemplateOutputID > 0 {
@@ -585,6 +548,36 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite)
if create.EnableSSL {
enableSSL := func(t *task.Task) error {
websiteModel, err := websiteSSLRepo.GetFirst(repo.WithByID(create.WebsiteSSLID))
if err != nil {
return err
}
website.Protocol = constant.ProtocolHTTPS
website.WebsiteSSLID = create.WebsiteSSLID
appSSLReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: true,
WebsiteSSLID: websiteModel.ID,
Type: "existed",
HttpConfig: "HTTPToHTTPS",
SSLProtocol: []string{"TLSv1.3", "TLSv1.2"},
Algorithm: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED",
Hsts: true,
HstsIncludeSubDomains: true,
}
if err = applySSL(website, *websiteModel, appSSLReq); err != nil {
return err
}
if err = websiteRepo.Save(context.Background(), website); err != nil {
return err
}
return nil
}
createTask.AddSubTaskWithIgnoreErr(i18n.GetMsgByKey("EnableSSL"), enableSSL)
}
if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 {
createFtpUser := func(t *task.Task) error {
indexDir := GetSitePath(*website, SiteIndexDir)
+30 -11
View File
@@ -277,12 +277,39 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
return nil, err
}
}
interPrivateKey, interPublicKey, _, err := createPrivateKey(websiteSSL.KeyType)
if err != nil {
return nil, err
}
notAfter := time.Now()
if req.Unit == "year" {
notAfter = notAfter.AddDate(req.Time, 0, 0)
} else {
notAfter = notAfter.AddDate(0, 0, req.Time)
}
interCsr := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix() + 2),
Subject: rootCsr.Subject,
NotBefore: time.Now(),
NotAfter: notAfter,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 0,
MaxPathLenZero: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
}
interDer, err := x509.CreateCertificate(rand.Reader, interCsr, rootCsr, interPublicKey, rootPrivateKey)
if err != nil {
return nil, err
}
interCert, err := x509.ParseCertificate(interDer)
if err != nil {
return nil, err
}
interCertBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: interCert.Raw,
}
_, publicKey, privateKeyBytes, err := createPrivateKey(websiteSSL.KeyType)
if err != nil {
return nil, err
@@ -303,13 +330,13 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
NotAfter: notAfter,
BasicConstraintsValid: true,
IsCA: false,
KeyUsage: leafKeyUsage(websiteSSL.KeyType),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: domains,
IPAddresses: ips,
}
der, err := x509.CreateCertificate(rand.Reader, csr, rootCsr, publicKey, rootPrivateKey)
der, err := x509.CreateCertificate(rand.Reader, csr, interCert, publicKey, interPrivateKey)
if err != nil {
return nil, err
}
@@ -322,7 +349,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
Type: "CERTIFICATE",
Bytes: cert.Raw,
}
websiteSSL.Pem = string(pem.EncodeToMemory(certBlock))
websiteSSL.Pem = string(pem.EncodeToMemory(certBlock)) + string(pem.EncodeToMemory(rootCertBlock)) + string(pem.EncodeToMemory(interCertBlock))
websiteSSL.PrivateKey = string(privateKeyBytes)
websiteSSL.ExpireDate = cert.NotAfter
websiteSSL.StartDate = cert.NotBefore
@@ -366,14 +393,6 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
return websiteSSL, nil
}
func leafKeyUsage(keyType string) x509.KeyUsage {
usage := x509.KeyUsageDigitalSignature
if ssl.KeyType(keyType) != certcrypto.EC256 && ssl.KeyType(keyType) != certcrypto.EC384 {
usage |= x509.KeyUsageKeyEncipherment
}
return usage
}
func createPrivateKey(keyType string) (privateKey any, publicKey any, privateKeyBytes []byte, err error) {
privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(keyType))
if err != nil {
+1 -1
View File
@@ -32,7 +32,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
return nil, err
}
go func() {
_ = ensureFirewallPorts(addPorts)
_ = OperateFirewallPort(nil, addPorts)
}()
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
+126 -141
View File
@@ -11,7 +11,6 @@ import (
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"
@@ -256,12 +255,7 @@ func createWebsiteFolder(website *model.Website, runtime *model.Runtime) error {
return nil
}
type websiteInitialSSL struct {
certificate model.WebsiteSSL
request request.WebsiteHTTPSOp
}
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig, initialSSL *websiteInitialSSL) error {
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
@@ -331,13 +325,6 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
setListen(server, strconv.Itoa(domain.Port), website.IPV6, false, website.DefaultServer, false)
}
server.UpdateServerName(serverNames)
if initialSSL != nil {
plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort)
applyWebsiteSSLConfig(server, *website, plan, initialSSL.request)
if err = createPemFile(*website, initialSSL.certificate); err != nil {
return err
}
}
siteFolder := path.Join("/www", "sites", website.Alias)
server.UpdateDirective("access_log", []string{path.Join(siteFolder, "log", "access.log"), "main"})
@@ -802,22 +789,52 @@ func createPemFile(website model.Website, websiteSSL model.WebsiteSSL) error {
return nil
}
type websiteTLSPlan struct {
httpPorts []int
httpsPorts []int
redirectPort int
defaultHTTPPort int
hasDefaultHTTP bool
func getHttpsPort(websiteID uint) map[int]struct{} {
domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID))
if err != nil {
return nil
}
httpsPorts := make(map[int]struct{})
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
hasDefaultPort := false
for _, domain := range domains {
if domain.Port == nginxInstall.HttpPort {
hasDefaultPort = true
}
if domain.SSL {
httpsPorts[domain.Port] = struct{}{}
}
}
if hasDefaultPort {
httpsPorts[nginxInstall.HttpsPort] = struct{}{}
}
if len(httpsPorts) == 0 {
for _, domain := range domains {
if !domain.SSL {
httpsPorts[domain.Port] = struct{}{}
}
}
}
return httpsPorts
}
func buildWebsiteTLSPlan(domains []model.WebsiteDomain, defaultHTTPPort, defaultHTTPSPort int) websiteTLSPlan {
func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.WebsiteHTTPSOp) error {
nginxFull, err := getNginxFull(website)
if err != nil {
return nil
}
domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID))
if err != nil {
return nil
}
httpPorts := make(map[int]struct{})
httpsPorts := make(map[int]struct{})
plan := websiteTLSPlan{defaultHTTPPort: defaultHTTPPort}
sslPort := 0
hasDefaultPort := false
for _, domain := range domains {
if domain.Port == defaultHTTPPort {
plan.hasDefaultHTTP = true
if domain.Port == nginxFull.Install.HttpPort {
hasDefaultPort = true
}
if domain.SSL {
httpsPorts[domain.Port] = struct{}{}
@@ -825,75 +842,112 @@ func buildWebsiteTLSPlan(domains []model.WebsiteDomain, defaultHTTPPort, default
httpPorts[domain.Port] = struct{}{}
}
}
if plan.hasDefaultHTTP {
httpsPorts[defaultHTTPSPort] = struct{}{}
if hasDefaultPort {
httpsPorts[nginxFull.Install.HttpsPort] = struct{}{}
}
if len(httpsPorts) == 0 {
for port := range httpPorts {
httpsPorts[port] = struct{}{}
}
}
for port := range httpsPorts {
delete(httpPorts, port)
}
for port := range httpPorts {
plan.httpPorts = append(plan.httpPorts, port)
}
for port := range httpsPorts {
plan.httpsPorts = append(plan.httpsPorts, port)
}
sort.Ints(plan.httpPorts)
sort.Ints(plan.httpsPorts)
if plan.hasDefaultHTTP {
plan.redirectPort = defaultHTTPSPort
} else if len(plan.httpsPorts) > 0 {
plan.redirectPort = plan.httpsPorts[0]
}
return plan
}
config := nginxFull.SiteConfig.Config
server := config.FindServers()[0]
func getHttpsPort(websiteID uint) map[int]struct{} {
domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID))
if err != nil {
return nil
}
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort)
httpsPorts := make(map[int]struct{}, len(plan.httpsPorts))
for _, port := range plan.httpsPorts {
httpsPorts[port] = struct{}{}
}
return httpsPorts
}
defaultHttpPort := strconv.Itoa(nginxFull.Install.HttpPort)
defaultHttpPortIPV6 := "[::]:" + defaultHttpPort
func buildWebsiteSSLParams(alias string, req request.WebsiteHTTPSOp, redirectPort int) []dto.NginxParam {
for port := range httpsPorts {
sslPort = port
portStr := strconv.Itoa(port)
server.RemoveListenByBind(portStr)
server.RemoveListenByBind("[::]:" + portStr)
setListen(server, portStr, website.IPV6, req.Http3, website.DefaultServer, true)
}
server.UpdateDirective("http2", []string{"on"})
switch req.HttpConfig {
case constant.HTTPSOnly:
server.RemoveListenByBind(defaultHttpPort)
server.RemoveListenByBind(defaultHttpPortIPV6)
server.RemoveDirective("if", []string{"($scheme"})
case constant.HTTPToHTTPS:
if hasDefaultPort {
server.UpdateListen(defaultHttpPort, website.DefaultServer)
if website.IPV6 {
server.UpdateListen(defaultHttpPortIPV6, website.DefaultServer)
}
}
server.AddHTTP2HTTPS(sslPort)
case constant.HTTPAlso:
if hasDefaultPort {
server.UpdateListen(defaultHttpPort, website.DefaultServer)
if website.IPV6 {
server.UpdateListen(defaultHttpPortIPV6, website.DefaultServer)
}
}
server.RemoveDirective("if", []string{"($scheme"})
}
if !req.Hsts {
server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""})
server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000; includeSubDomains\""})
}
if !req.Http3 {
for port := range httpsPorts {
server.RemoveListen(strconv.Itoa(port), "quic")
if website.IPV6 {
httpsPortIPV6 := "[::]:" + strconv.Itoa(port)
server.RemoveListen(httpsPortIPV6, "quic")
}
}
server.RemoveDirective("add_header", []string{"Alt-Svc"})
}
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return err
}
if err = createPemFile(*website, websiteSSL); err != nil {
return err
}
nginxParams := getNginxParamsFromStaticFile(dto.SSL, []dto.NginxParam{})
for i := range nginxParams {
switch nginxParams[i].Name {
case "ssl_certificate":
nginxParams[i].Params = []string{path.Join("/www", "sites", alias, "ssl", "fullchain.pem")}
case "ssl_certificate_key":
nginxParams[i].Params = []string{path.Join("/www", "sites", alias, "ssl", "privkey.pem")}
case "ssl_protocols":
for i, param := range nginxParams {
if param.Name == "ssl_certificate" {
nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "fullchain.pem")}
}
if param.Name == "ssl_certificate_key" {
nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "privkey.pem")}
}
if param.Name == "ssl_protocols" {
nginxParams[i].Params = req.SSLProtocol
if len(req.SSLProtocol) == 0 {
nginxParams[i].Params = []string{"TLSv1.3", "TLSv1.2"}
}
case "ssl_ciphers":
}
if param.Name == "ssl_ciphers" {
nginxParams[i].Params = []string{req.Algorithm}
if len(req.Algorithm) == 0 {
nginxParams[i].Params = []string{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED"}
}
case "error_page":
if len(nginxParams[i].Params) >= 2 && nginxParams[i].Params[0] == "497" && redirectPort != 443 && nginxParams[i].Params[1] == "https://$host$request_uri" {
nginxParams[i].Params[1] = fmt.Sprintf("https://$host:%d$request_uri", redirectPort)
}
if param.Name == "error_page" {
if len(param.Params) < 2 {
continue
}
code := param.Params[0]
if code == "497" {
if sslPort != 443 && param.Params[1] == "https://$host$request_uri" {
param.Params[1] = fmt.Sprintf("https://$host:%d$request_uri", sslPort)
}
}
}
}
if req.Hsts {
hstsValue := "\"max-age=31536000\""
var hstsValue string
if req.HstsIncludeSubDomains {
hstsValue = "\"max-age=31536000; includeSubDomains\""
} else {
hstsValue = "\"max-age=31536000\""
}
nginxParams = append(nginxParams, dto.NginxParam{
Name: "add_header",
@@ -906,80 +960,11 @@ func buildWebsiteSSLParams(alias string, req request.WebsiteHTTPSOp, redirectPor
Params: []string{"Alt-Svc", "'h3=\":443\"; ma=2592000'"},
})
}
return nginxParams
}
func applyWebsiteSSLConfig(server *components.Server, website model.Website, plan websiteTLSPlan, req request.WebsiteHTTPSOp) {
for _, port := range plan.httpsPorts {
portStr := strconv.Itoa(port)
server.RemoveListenByBind(portStr)
server.RemoveListenByBind("[::]:" + portStr)
setListen(server, portStr, website.IPV6, req.Http3, website.DefaultServer, true)
}
server.UpdateDirective("http2", []string{"on"})
defaultHTTPPort := strconv.Itoa(plan.defaultHTTPPort)
switch req.HttpConfig {
case constant.HTTPSOnly:
if plan.hasDefaultHTTP {
server.RemoveListenByBind(defaultHTTPPort)
server.RemoveListenByBind("[::]:" + defaultHTTPPort)
}
server.RemoveDirective("if", []string{"($scheme"})
case constant.HTTPToHTTPS:
if plan.hasDefaultHTTP {
setListen(server, defaultHTTPPort, website.IPV6, false, website.DefaultServer, false)
}
if plan.redirectPort > 0 {
server.AddHTTP2HTTPS(plan.redirectPort)
}
case constant.HTTPAlso:
if plan.hasDefaultHTTP {
setListen(server, defaultHTTPPort, website.IPV6, false, website.DefaultServer, false)
}
server.RemoveDirective("if", []string{"($scheme"})
}
if !req.Hsts {
server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""})
server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000; includeSubDomains\""})
}
if !req.Http3 {
for _, port := range plan.httpsPorts {
server.RemoveListen(strconv.Itoa(port), "quic")
if website.IPV6 {
server.RemoveListen("[::]:"+strconv.Itoa(port), "quic")
}
}
server.RemoveDirective("add_header", []string{"Alt-Svc"})
}
for _, param := range buildWebsiteSSLParams(website.Alias, req, plan.redirectPort) {
server.UpdateDirective(param.Name, param.Params)
}
}
func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.WebsiteHTTPSOp) error {
nginxFull, err := getNginxFull(website)
if err != nil {
return nil
}
domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID))
if err != nil {
return nil
}
config := nginxFull.SiteConfig.Config
server := config.FindServers()[0]
plan := buildWebsiteTLSPlan(domains, nginxFull.Install.HttpPort, nginxFull.Install.HttpsPort)
applyWebsiteSSLConfig(server, *website, plan, req)
if err = createPemFile(*website, websiteSSL); err != nil {
if err := updateNginxConfig(constant.NginxScopeServer, nginxParams, website); err != nil {
return err
}
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return err
}
return nginxCheckAndReload(nginxFull.SiteConfig.OldContent, nginxFull.SiteConfig.FilePath, nginxFull.Install.ContainerName)
return nil
}
func getParamArray(key string, param interface{}) []string {
+261 -322
View File
@@ -116,16 +116,6 @@
"formatZH": "删除告警配置 [id]",
"formatEN": "delete alert config [id]"
},
"/alert/config/status": {
"bodyKeys": [
"id",
"status"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置状态 [id][status]",
"formatEN": "update alert config status [id][status]"
},
"/alert/config/update": {
"bodyKeys": [
"id",
@@ -1755,6 +1745,262 @@
"formatZH": "更新 [name]",
"formatEN": "update user [name]"
},
"/core/enterprise/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/core/enterprise/vms/del": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/enterprise/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/core/enterprise/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/core/enterprise/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/core/enterprise/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/core/enterprise/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/core/enterprise/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/core/enterprise/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/core/enterprise/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/core/enterprise/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/core/enterprise/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/core/enterprise/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/core/enterprise/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/core/enterprise/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/core/enterprise/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/core/enterprise/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/core/enterprise/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/core/enterprise/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/core/enterprise/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/core/enterprise/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/core/enterprise/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/core/groups": {
"bodyKeys": [
"name",
@@ -2459,271 +2705,6 @@
"formatZH": "同步 SSL 证书 [primaryDomain]",
"formatEN": "sync SSL certificate [primaryDomain]"
},
"/core/xpack/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/core/xpack/vms/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/core/xpack/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/core/xpack/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/core/xpack/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/core/xpack/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/core/xpack/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/core/xpack/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/core/xpack/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/core/xpack/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/core/xpack/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/core/xpack/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/core/xpack/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/core/xpack/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/core/xpack/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/core/xpack/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/core/xpack/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/core/xpack/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/core/xpack/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/core/xpack/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/core/xpack/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/cronjobs": {
"bodyKeys": [
"type",
@@ -3522,15 +3503,6 @@
"formatZH": "下载 url =\u003e [path]/[name]",
"formatEN": "Download url =\u003e [path]/[name]"
},
"/files/wget/process/remove": {
"bodyKeys": [
"keys"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "移除已结束下载记录 [keys]",
"formatEN": "Remove finished download records [keys]"
},
"/files/wget/stop": {
"bodyKeys": [
"key"
@@ -3655,11 +3627,13 @@
"formatEN": "[operation] Docker port guard"
},
"/hosts/firewall/docker/policies/batch": {
"bodyKeys": [],
"bodyKeys": [
"mode"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "批量更新 Docker 端口防护策略",
"formatEN": "batch update Docker port guard policies"
"formatZH": "批量更新 Docker 端口防护策略 [mode]",
"formatEN": "batch update Docker port guard policies [mode]"
},
"/hosts/firewall/docker/policies/delete/batch": {
"bodyKeys": [
@@ -3716,13 +3690,6 @@
"formatZH": "添加防火墙规则",
"formatEN": "create firewall rules"
},
"/hosts/firewall/rules/adopt": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "纳管防火墙规则",
"formatEN": "adopt firewall rule"
},
"/hosts/firewall/rules/delete": {
"bodyKeys": [],
"paramKeys": [],
@@ -3777,34 +3744,6 @@
"formatZH": "防火墙子系统 [subsystem] 后端 [operation] [backend]",
"formatEN": "[operation] firewall [subsystem] backend [backend]"
},
"/hosts/firewall/settings/whitelist": {
"bodyKeys": [
"rule"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建防火墙端口白名单",
"formatEN": "create firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/delete": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除防火墙端口白名单",
"formatEN": "delete firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/update": {
"bodyKeys": [
"oldRule",
"rule"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "编辑防火墙端口白名单",
"formatEN": "update firewall port whitelist"
},
"/hosts/monitor/clean": {
"bodyKeys": [],
"paramKeys": [],
+2 -6
View File
@@ -1,8 +1,4 @@
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 5;
gzip_proxied any;
gzip_types text/plain text/css text/xml text/javascript application/json application/ld+json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/wasm image/svg+xml font/ttf font/otf;
gzip_types text/plain text/css text/xml text/javascript text/x-component application/json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/x-font-ttf application/vnd.ms-fontobject image/svg+xml image/x-icon font/opentype;
+6 -6
View File
@@ -1,11 +1,10 @@
package constant
const (
FirewallProviderFirewalld = "firewalld"
FirewallProviderUFW = "ufw"
FirewallProviderIptables = "iptables"
FirewallProviderNftables = "nftables"
FirewallBackendNotInstalled = "backend_not_installed"
FirewallProviderFirewalld = "firewalld"
FirewallProviderUFW = "ufw"
FirewallProviderIptables = "iptables"
FirewallProviderNftables = "nftables"
FirewallFamilyIPv4 = "ipv4"
FirewallFamilyIPv6 = "ipv6"
@@ -27,11 +26,12 @@ const (
FirewallPingStatusKey = "BanPing"
FirewallPortWhiteList = "FirewallPortWhiteList"
FirewallPortWhiteListValue = `[{"port":"80","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"udp","sources":["0.0.0.0/0","::/0"]}]`
FirewallPortWhiteListValue = "80/tcp,443/tcp,443/udp"
)
const (
FirewallSystemAcceptedPortSourcePrefix = "accepted-port:"
FirewallRuleCheckVersion = 1
FirewallRuleOriginCreated = "created"
FirewallRuleOriginAdopted = "adopted"
+14 -15
View File
@@ -31,8 +31,8 @@ require (
github.com/klauspost/compress v1.19.2
github.com/mattn/go-shellwords v1.0.14
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.73
github.com/minio/minio-go/v7 v7.3.0
github.com/miekg/dns v1.1.72
github.com/minio/minio-go/v7 v7.2.1
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/opencontainers/image-spec v1.1.1
github.com/oschwald/maxminddb-golang v1.13.1
@@ -42,7 +42,7 @@ require (
github.com/qiniu/go-sdk/v7 v7.27.0
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.26.7
github.com/sirupsen/logrus v1.10.2
github.com/sirupsen/logrus v1.9.4
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
@@ -52,12 +52,12 @@ require (
github.com/tklauser/go-sysconf v0.4.0
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
github.com/upyun/go-sdk v2.1.0+incompatible
go.mongodb.org/mongo-driver/v2 v2.8.2
golang.org/x/crypto v0.55.0
golang.org/x/net v0.58.0
go.mongodb.org/mongo-driver/v2 v2.8.0
golang.org/x/crypto v0.54.0
golang.org/x/net v0.57.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.41.0
golang.org/x/text v0.40.0
golang.org/x/time v0.15.0
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
gopkg.in/ini.v1 v1.67.3
@@ -143,7 +143,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/fs v0.1.0 // indirect
@@ -209,18 +209,17 @@ require (
go.mongodb.org/mongo-driver v1.17.9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
google.golang.org/grpc v1.83.1 // indirect
golang.org/x/tools v0.47.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/fileutil v1.4.0 // indirect
modernc.org/libc v1.72.0 // indirect
+38 -38
View File
@@ -527,8 +527,8 @@ github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -576,16 +576,16 @@ github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60V
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.3.0 h1:HM4pFCSQq/TK+j0/zmorSh5ddh81iDgRgU0BG0Vz/YU=
github.com/minio/minio-go/v7 v7.3.0/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk=
github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -698,6 +698,8 @@ github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6J
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
@@ -757,8 +759,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -800,9 +802,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
@@ -877,8 +878,8 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver/v2 v2.8.2 h1:b6o2m7zL8g2URuO8urBedAylxojybKXNZTxgkOcl+2w=
go.mongodb.org/mongo-driver/v2 v2.8.2/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -889,20 +890,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
@@ -915,9 +916,8 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
@@ -951,8 +951,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -994,8 +994,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1050,8 +1050,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1186,8 +1186,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1249,8 +1249,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1315,8 +1315,8 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 h1:aLsVTW0lZ8+IY5u/ERjZSCvAmhuR7slKzyha3YikDNA=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478/go.mod h1:YJAzKjfHIUHb9T+bfu8L7mthAp7VVXQBUs1PLdBWS7M=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -1336,8 +1336,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: 'Failed to parse the project name from Docker Compos
ErrComposeProjectNameEmpty: 'Unable to derive a project name from Docker Compose config or the file parent directory. Enter a name and try again.'
ErrComposeNameInvalid: 'Invalid Compose project name: it must start with a lowercase letter or number, contain only lowercase letters, numbers, hyphens, and underscores, and be 1-256 characters long.'
ErrAppVersionUnavailable: 'This application version has been removed from the remote service. Please select another version and try again.'
ErrVllmGB10ProOnly: 'This vLLM version is available only in 1Panel Professional Edition.'
ErrAppWarn: 'App status abnormal; check logs'
ErrAppParamKey: 'Invalid app parameter: {{ .name }}'
ErrAppUpgrade: 'App upgrade failed: {{ .name }} {{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: 'Invalid domain format: {{ .name }}'
ErrDefaultAlias: 'default is reserved; use another alias'
ErrParentWebsite: 'Delete subsite {{ .name }} first'
ErrBuildDirNotFound: 'The build directory does not exist'
ErrBrotliDisabled: 'The brotli module is not enabled, enable and build it first'
ErrBrotliUnsupported: 'The panel could not wire brotli settings into nginx.conf automatically; check the file or upgrade OpenResty'
ErrModuleBuildUnsupported: 'This OpenResty version cannot build modules, upgrade it first'
ErrImageNotExist: 'Runtime image not found: {{ .name }}'
ErrProxyIsUsed: 'Load balancer is used by reverse proxy'
ErrSSLValid: 'Certificate file is invalid'
@@ -506,6 +502,16 @@ Container: 'Container'
AppLink: 'Linked Application'
EnableSSL: 'Enable HTTPS'
AppStore: 'App Store'
Firewall: 'Firewall'
FirewallSyncStep: 'Synchronize rules to {{ .name }}'
FirewallSyncResult: 'Synchronization result: {{ .succeeded }} succeeded, {{ .existing }} existing, {{ .failed }} failed'
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
FirewallSyncFailedResetSkipped: '{{ .failed }} firewall rules failed to synchronize; the source firewall was not reset'
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'
FirewallResetSourceResult: 'Source firewall reset completed; {{ .removed }} database rules were deleted'
FirewallVerifyTargetStep: 'Verify target firewall rules'
FirewallTargetRuleIneffective: 'The target rule is no longer effective'
FirewallVerifyRuleFailed: 'Failed to verify synchronized rule {{ .name }}: {{ .detail }}'
TaskSync: 'Sync'
LocalApp: 'Local Application'
SubTask: 'Subtask'
@@ -661,7 +667,6 @@ XfsNotFound: 'xfs not found; install xfsprogs first'
# terminal
TerminalAIBlockedRiskyCommand: 'blocked risky command: {{ .command }}'
TerminalAIThinking: 'AI is thinking...'
TerminalOutputTruncated: '[output truncated, showing recent output only]'
TerminalAIReadyToExecute: 'Thinking complete, press Enter to execute (duration: {{ .duration }}, tokens: {{ .tokens }})'
TerminalAIRequestFailed: 'AI request failed: {{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'The Docker DOCKER-USER chain was not found. Restart Docker and try again'
ErrDockerNftablesChainUnavailable: 'The Docker nftables IPv4 firewall chain was not found. Verify that the current Docker version supports the nftables firewall backend, restart Docker, and try again'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD defaults to DROP. Adjust the policy and retry'
ErrUFWRuleAdopt: 'Failed to adopt the UFW rule: {{ .detail }}. If the installed UFW version is earlier than 0.35, upgrade UFW and try again'
Firewall: 'Firewall'
FirewallTaskHost: 'Host firewall'
FirewallTaskForwarding: 'Port forwarding'
FirewallTaskDocker: 'Container port protection'
FirewallRuleTaskCreate: 'Create firewall rules [{{ .name }}]'
FirewallRuleTaskUpdate: 'Update firewall rules [{{ .name }}]'
FirewallRuleTaskDelete: 'Delete firewall rules [{{ .name }}]'
FirewallRuleTaskSync: 'Synchronize firewall rules [{{ .name }}]'
FirewallTaskInitialize: 'Initialize firewall [{{ .name }}]'
FirewallCreateRulesStep: 'Create firewall rules'
FirewallCreateRulesResult: 'Creation result: {{ .succeeded }} succeeded, {{ .failed }} failed, {{ .skipped }} not executed'
FirewallRuleOperationResult: 'Operation result: {{ .succeeded }} succeeded, {{ .failed }} failed'
FirewallCreateRuleSkipped: 'Not executed'
FirewallCreateBatchStep: 'Submit {{ .count }} rules as a batch to {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; converted to {{ .count }} rules'
FirewallCreateRuleExecutionFailed: 'Rule creation failed; no database record was saved and executed commands were not rolled back'
FirewallCreateRulePersistenceFailed: 'The rule was created, but its management record could not be saved'
FirewallAdoptRulePersistenceFailed: 'The rule was adopted, but its management record could not be saved'
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed, {{ .unchanged }} already matching (no changes needed)'
FirewallSyncRuleUnchanged: 'Already matching; no changes needed'
FirewallSyncStep: 'Synchronize rules to {{ .name }}'
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'
FirewallResetSourceResult: 'Source firewall reset completed; {{ .removed }} database rules were deleted'
FirewallInitializeChainsStep: 'Initialize and bind {{ .name }} base chains'
FirewallRestoreRulesStep: 'Restore database rules to {{ .name }}'
FirewallSyncWhitelistStep: 'Synchronize firewall port whitelist'
FirewallEnableForwardingStep: 'Enable and bind port forwarding chains'
FirewallRestoreForwardingRulesStep: 'Restore database port forwarding rules'
FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule."
FirewallWhitelistReleased: "{{ .name }}: whitelist protection released; allow rule retained. To close the port, delete the rule manually from the rule list"
FirewallWhitelistRequired: "{{ .name }}: protected by mandatory system port rules"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: 'No se pudo analizar el nombre del proyecto desde la
ErrComposeProjectNameEmpty: 'No se pudo derivar un nombre de proyecto desde la configuración de Docker Compose ni desde el directorio padre del archivo. Introduzca un nombre e inténtelo de nuevo.'
ErrComposeNameInvalid: 'Nombre de proyecto Compose no válido: debe comenzar con una letra minúscula o un número, contener solo letras minúsculas, números, guiones y guiones bajos, y tener entre 1 y 256 caracteres.'
ErrAppVersionUnavailable: 'Esta versión de la aplicación se ha eliminado del servicio remoto. Seleccione otra versión e inténtelo de nuevo.'
ErrVllmGB10ProOnly: 'Esta versión de vLLM solo está disponible en 1Panel Professional Edition.'
ErrAppWarn: 'Estado anómalo, revise el log'
ErrAppParamKey: 'El campo de parámetro {{ .name }} es anómalo'
ErrAppUpgrade: 'La actualización de la aplicación {{ .name }} falló {{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: 'El formato del dominio {{ .name }} es incorrecto'
ErrDefaultAlias: 'default es un código reservado, use otro'
ErrParentWebsite: 'Primero debe eliminar el sub-sitio {{ .name }}'
ErrBuildDirNotFound: 'El directorio de compilación no existe'
ErrBrotliDisabled: 'El módulo brotli no está habilitado, actívelo y compílelo primero'
ErrBrotliUnsupported: 'El panel no pudo conectar automáticamente la configuración brotli a nginx.conf; revise el archivo o actualice OpenResty'
ErrModuleBuildUnsupported: 'Esta versión de OpenResty no puede compilar módulos, actualícela primero'
ErrImageNotExist: 'La imagen del entorno {{ .name }} no existe, edítela de nuevo'
ErrProxyIsUsed: 'El balanceo de carga ya está usado por un proxy reverso, no se puede eliminar'
ErrSSLValid: 'Archivo de certificado anómalo, revise el estado del certificado'
@@ -506,6 +502,16 @@ Container: 'Contenedor'
AppLink: 'Aplicación enlazada'
EnableSSL: 'Habilitar HTTPS'
AppStore: 'Tienda de aplicaciones'
Firewall: 'Firewall'
FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
FirewallSyncResult: 'Resultado de sincronización: {{ .succeeded }} correctas, {{ .existing }} existentes, {{ .failed }} fallidas'
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
FirewallSyncFailedResetSkipped: 'No se pudieron sincronizar {{ .failed }} reglas; el firewall de origen no se restableció'
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'
FirewallResetSourceResult: 'Firewall de origen restablecido; se eliminaron {{ .removed }} reglas de la base de datos'
FirewallVerifyTargetStep: 'Verificar las reglas del firewall de destino'
FirewallTargetRuleIneffective: 'La regla de destino ya no es efectiva'
FirewallVerifyRuleFailed: 'No se pudo verificar la regla sincronizada {{ .name }}: {{ .detail }}'
TaskSync: 'Sincronizar'
LocalApp: 'Aplicación local'
SubTask: 'Subtarea'
@@ -661,7 +667,6 @@ XfsNotFound: 'No se detectó el sistema de archivos xfs instale xfsprogs primero
# terminal
TerminalAIBlockedRiskyCommand: 'Comando de riesgo bloqueado: {{ .command }}'
TerminalAIThinking: 'La IA está pensando...'
TerminalOutputTruncated: '[salida truncada, se muestra solo la salida reciente]'
TerminalAIReadyToExecute: 'Pensamiento completado, pulsa Enter para ejecutar (duración: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'La solicitud de AI ha fallado: {{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'No se encontró la cadena DOCKER-USER de Docker. Reinicie Docker y vuelva a intentarlo'
ErrDockerNftablesChainUnavailable: 'No se encontró la cadena de firewall IPv4 de nftables de Docker. Compruebe que la versión actual de Docker admita el backend de firewall nftables, reinicie Docker y vuelva a intentarlo'
ErrDockerForwardPolicyDrop: 'La política predeterminada de FORWARD para {{ .family }} es DROP. Ajústela y vuelva a intentarlo'
ErrUFWRuleAdopt: 'No se pudo administrar la regla UFW: {{ .detail }}. Si la versión actual de UFW es anterior a 0.35, actualice UFW y vuelva a intentarlo'
Firewall: 'Firewall'
FirewallTaskHost: 'Cortafuegos del host'
FirewallTaskForwarding: 'Reenvío de puertos'
FirewallTaskDocker: 'Protección de puertos de contenedores'
FirewallRuleTaskCreate: 'Crear reglas de cortafuegos [{{ .name }}]'
FirewallRuleTaskUpdate: 'Actualizar reglas de cortafuegos [{{ .name }}]'
FirewallRuleTaskDelete: 'Eliminar reglas de cortafuegos [{{ .name }}]'
FirewallRuleTaskSync: 'Sincronizar reglas de cortafuegos [{{ .name }}]'
FirewallTaskInitialize: 'Inicializar cortafuegos [{{ .name }}]'
FirewallCreateRulesStep: 'Crear reglas de cortafuegos'
FirewallCreateRulesResult: 'Resultado de creación: {{ .succeeded }} correctas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
FirewallRuleOperationResult: 'Resultado de la operación: {{ .succeeded }} correctas, {{ .failed }} fallidas'
FirewallCreateRuleSkipped: 'Sin ejecutar'
FirewallCreateBatchStep: 'Enviar {{ .count }} reglas en un lote a {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido en {{ .count }} reglas'
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. No se guardó ningún registro en la base de datos ni se revirtieron los comandos ejecutados'
FirewallCreateRulePersistenceFailed: 'La regla se creó, pero no se pudo guardar su registro de gestión'
FirewallAdoptRulePersistenceFailed: 'Se ejecutó la adopción de la regla, pero no se pudo guardar su registro de gestión'
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar, {{ .unchanged }} ya coinciden (sin cambios necesarios)'
FirewallSyncRuleUnchanged: 'Ya coincide; no requiere cambios'
FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'
FirewallResetSourceResult: 'Firewall de origen restablecido; se eliminaron {{ .removed }} reglas de la base de datos'
FirewallInitializeChainsStep: 'Inicializar y vincular las cadenas base de {{ .name }}'
FirewallRestoreRulesStep: 'Restaurar las reglas de la base de datos en {{ .name }}'
FirewallSyncWhitelistStep: 'Sincronizar la lista de puertos permitidos del firewall'
FirewallEnableForwardingStep: 'Habilitar y vincular las cadenas de reenvío de puertos'
FirewallRestoreForwardingRulesStep: 'Restaurar las reglas de reenvío de puertos de la base de datos'
FirewallInspectDockerGuardStep: 'Inspeccionar el backend del firewall de Docker y las políticas'
FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}'
FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker'
ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva."
FirewallWhitelistReleased: "{{ .name }}: protección de la lista de permitidos retirada; se conserva la regla de permiso. Para cerrar el puerto, elimine la regla manualmente de la lista"
FirewallWhitelistRequired: "{{ .name }}: protegido por las reglas de puertos obligatorios del sistema"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: 'تجزیه نام پروژه از خروجی پیک
ErrComposeProjectNameEmpty: 'نام پروژه از پیکربندی Docker Compose یا پوشه والد فایل قابل استخراج نیست. یک نام وارد کرده و دوباره تلاش کنید.'
ErrComposeNameInvalid: 'نام پروژه Compose نامعتبر است: باید با حرف کوچک یا عدد شروع شود، فقط شامل حروف کوچک، اعداد، خط تیره و زیرخط باشد و ۱ تا ۲۵۶ نویسه داشته باشد.'
ErrAppVersionUnavailable: 'این نسخه برنامه از سرویس راه دور حذف شده است. لطفاً نسخه دیگری را انتخاب کرده و دوباره تلاش کنید.'
ErrVllmGB10ProOnly: 'این نسخه vLLM فقط در نسخه حرفه‌ای 1Panel در دسترس است.'
ErrAppWarn: 'وضعیت برنامه غیرعادی است؛ لاگ‌ها را بررسی کنید'
ErrAppParamKey: 'پارامتر برنامه نامعتبر است: {{ .name }}'
ErrAppUpgrade: 'ارتقاء برنامه ناموفق بود: {{ .name }} {{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: 'فرمت دامنه نامعتبر است: {{ .name }}'
ErrDefaultAlias: 'default رزرو شده است؛ از نام مستعار دیگری استفاده کنید'
ErrParentWebsite: 'ابتدا زیرسایت {{ .name }} را حذف کنید'
ErrBuildDirNotFound: 'دایرکتوری ساخت وجود ندارد'
ErrBrotliDisabled: 'ماژول brotli فعال نیست، ابتدا آن را فعال و بیلد کنید'
ErrBrotliUnsupported: 'پنل نتوانست تنظیمات brotli را به‌صورت خودکار به nginx.conf متصل کند؛ فایل را بررسی کنید یا OpenResty را ارتقا دهید'
ErrModuleBuildUnsupported: 'این نسخه OpenResty نمی‌تواند ماژول بسازد، ابتدا آن را ارتقا دهید'
ErrImageNotExist: 'تصویر محیط اجرا یافت نشد: {{ .name }}'
ErrProxyIsUsed: 'تعادل بار توسط پراکسی معکوس استفاده می‌شود'
ErrSSLValid: 'فایل گواهی نامعتبر است'
@@ -506,6 +502,16 @@ Container: 'کانتینر'
AppLink: 'برنامه متصل'
EnableSSL: 'فعال‌سازی HTTPS'
AppStore: 'فروشگاه برنامه'
Firewall: 'فایروال'
FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}'
FirewallSyncResult: 'نتیجه همگام‌سازی: {{ .succeeded }} موفق، {{ .existing }} موجود، {{ .failed }} ناموفق'
FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود'
FirewallSyncFailedResetSkipped: 'همگام‌سازی {{ .failed }} قانون ناموفق بود؛ فایروال مبدأ بازنشانی نشد'
FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}'
FirewallResetSourceResult: 'فایروال مبدأ بازنشانی شد و {{ .removed }} قانون پایگاه داده حذف شد'
FirewallVerifyTargetStep: 'بررسی قوانین فایروال مقصد'
FirewallTargetRuleIneffective: 'قانون مقصد دیگر مؤثر نیست'
FirewallVerifyRuleFailed: 'بررسی قانون همگام‌شده {{ .name }} ناموفق بود: {{ .detail }}'
TaskSync: 'همگام‌سازی'
LocalApp: 'برنامه محلی'
SubTask: 'وظیفه فرعی'
@@ -661,7 +667,6 @@ XfsNotFound: 'xfs یافت نشد؛ ابتدا xfsprogs را نصب کنید'
# ترمینال
TerminalAIBlockedRiskyCommand: 'دستور پرخطر مسدود شد: {{ .command }}'
TerminalAIThinking: 'هوش مصنوعی در حال فکر کردن است...'
TerminalOutputTruncated: '[خروجی کوتاه شد، فقط خروجی اخیر نمایش داده می‌شود]'
TerminalAIReadyToExecute: 'تفکر کامل شد، برای اجرا Enter را فشار دهید (مدت زمان: {{ .duration }}، توکن‌ها: {{ .tokens }})'
TerminalAIRequestFailed: 'درخواست هوش مصنوعی ناموفق بود: {{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راه‌اندازی مجدد کنید و دوباره تلاش کنید'
ErrDockerNftablesChainUnavailable: 'زنجیره فایروال IPv4 مربوط به nftables داکر یافت نشد. بررسی کنید نسخه فعلی داکر از بک‌اند فایروال nftables پشتیبانی کند، سپس داکر را راه‌اندازی مجدد کرده و دوباره تلاش کنید'
ErrDockerForwardPolicyDrop: 'سیاست پیش‌فرض FORWARD برای {{ .family }} برابر DROP است. آن را تغییر دهید و دوباره تلاش کنید'
ErrUFWRuleAdopt: 'مدیریت قانون UFW ناموفق بود: {{ .detail }}. اگر نسخه فعلی UFW قدیمی‌تر از 0.35 است، ابتدا UFW را ارتقا دهید و دوباره تلاش کنید'
Firewall: 'فایروال'
FirewallTaskHost: 'دیواره آتش میزبان'
FirewallTaskForwarding: 'هدایت پورت'
FirewallTaskDocker: 'محافظت از پورت کانتینر'
FirewallRuleTaskCreate: 'ایجاد قوانین دیواره آتش [{{ .name }}]'
FirewallRuleTaskUpdate: 'به‌روزرسانی قوانین دیواره آتش [{{ .name }}]'
FirewallRuleTaskDelete: 'حذف قوانین دیواره آتش [{{ .name }}]'
FirewallRuleTaskSync: 'همگام‌سازی قوانین دیواره آتش [{{ .name }}]'
FirewallTaskInitialize: 'راه‌اندازی اولیه دیواره آتش [{{ .name }}]'
FirewallCreateRulesStep: 'ایجاد قوانین دیوار آتش'
FirewallCreateRulesResult: 'نتیجه ایجاد: {{ .succeeded }} موفق، {{ .failed }} ناموفق، {{ .skipped }} اجرا نشده'
FirewallRuleOperationResult: 'نتیجه عملیات: {{ .succeeded }} موفق، {{ .failed }} ناموفق'
FirewallCreateRuleSkipped: 'اجرا نشده'
FirewallCreateBatchStep: 'ارسال {{ .count }} قانون به صورت دسته‌ای به {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}؛ به {{ .count }} قانون تبدیل شد'
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند'
FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود'
FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد'
FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)'
FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست'
FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}'
FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود'
FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}'
FirewallResetSourceResult: 'فایروال مبدأ بازنشانی شد و {{ .removed }} قانون پایگاه داده حذف شد'
FirewallInitializeChainsStep: 'راه‌اندازی و اتصال زنجیره‌های پایه {{ .name }}'
FirewallRestoreRulesStep: 'بازیابی قوانین پایگاه داده در {{ .name }}'
FirewallSyncWhitelistStep: 'همگام‌سازی فهرست مجاز پورت‌های فایروال'
FirewallEnableForwardingStep: 'فعال‌سازی و اتصال زنجیره‌های هدایت پورت'
FirewallRestoreForwardingRulesStep: 'بازیابی قوانین هدایت پورت پایگاه داده'
FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker و سیاست‌ها'
FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}'
FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker'
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید."
FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید"
FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: 'Docker Compose 設定出力からプロジェクト
ErrComposeProjectNameEmpty: 'Docker Compose 設定またはファイルの親ディレクトリからプロジェクト名を推定できません。名前を入力して再試行してください。'
ErrComposeNameInvalid: 'Compose プロジェクト名の形式が正しくありません。小文字または数字で始まり、小文字、数字、ハイフン、アンダースコアのみを使用した 1~256 文字にしてください。'
ErrAppVersionUnavailable: '現在のアプリケーションバージョンはリモートサービスから削除されています。別のバージョンを選択して再試行してください。'
ErrVllmGB10ProOnly: 'この vLLM バージョンは 1Panel プロフェッショナル版でのみ利用できます。'
ErrAppWarn: '異常な状態です。ログを確認してください'
ErrAppParamKey: 'パラメータ {{ .name }} フィールドが異常です'
ErrAppUpgrade: 'アプリケーション {{ .name }} のアップグレードに失敗しました {{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: '{{ .name }} ドメイン名の形式が正しくありませ
ErrDefaultAlias: 'デフォルトは予約済みのコードです。別のコードを使用してください'
ErrParentWebsite: 'まずサブサイト {{ .name }} を削除する必要があります'
ErrBuildDirNotFound: 'ビルド ディレクトリが存在しません'
ErrBrotliDisabled: 'brotli モジュールが有効になっていません。先に有効化してビルドしてください'
ErrBrotliUnsupported: 'パネルは brotli 設定を nginx.conf に自動で組み込めませんでした。ファイルを確認するか OpenResty をアップグレードしてください'
ErrModuleBuildUnsupported: 'この OpenResty バージョンではモジュールをビルドできません。先にアップグレードしてください'
ErrImageNotExist: 'オペレーティング環境 {{ .name }} イメージが存在しません。オペレーティング環境を再編集してください'
ErrProxyIsUsed: 'ロードバランシングはリバースプロキシによって使用されているため、削除できません'
ErrSSLValid: '証明書ファイルが異常です、証明書の状態を確認してください!'
@@ -506,6 +502,16 @@ Container: 'コンテナ'
AppLink: 'リンクされたアプリケーション'
EnableSSL: 'HTTPS を有効にする'
AppStore: 'Appストア'
Firewall: 'ファイアウォール'
FirewallSyncStep: '{{ .name }} にルールを同期'
FirewallSyncResult: '同期結果:成功 {{ .succeeded }} 件、既存 {{ .existing }} 件、失敗 {{ .failed }} 件'
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
FirewallSyncFailedResetSkipped: '{{ .failed }} 件のルールを同期できなかったため、移行元ファイアウォールはリセットされませんでした'
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'
FirewallResetSourceResult: '移行元ファイアウォールをリセットし、データベースルール {{ .removed }} 件を削除しました'
FirewallVerifyTargetStep: '移行先ファイアウォールルールを検証'
FirewallTargetRuleIneffective: '移行先ルールは有効ではありません'
FirewallVerifyRuleFailed: '同期ルール {{ .name }} の検証に失敗しました:{{ .detail }}'
TaskSync: '同期'
LocalApp: 'ローカルアプリケーション'
SubTask: 'サブタスク'
@@ -661,7 +667,6 @@ XfsNotFound: 'xfs ファイルシステムが検出されませんでした、
# ターミナル
TerminalAIBlockedRiskyCommand: '危険なコマンドをブロックしました: {{ .command }}'
TerminalAIThinking: 'AI が考えています...'
TerminalOutputTruncated: '[出力は切り詰められました。最近の出力のみ表示しています]'
TerminalAIReadyToExecute: '思考が完了しました。Enter で実行してください(所要時間 {{ .duration }}、token: {{ .tokens }})'
TerminalAIRequestFailed: 'AI リクエストに失敗しました: {{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください'
ErrDockerNftablesChainUnavailable: 'Docker の nftables IPv4 ファイアウォールチェーンが見つかりません。現在の Docker バージョンが nftables ファイアウォールバックエンドをサポートしていることを確認し、Docker を再起動してから再試行してください'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD の既定ポリシーが DROP です。変更してから再試行してください'
ErrUFWRuleAdopt: 'UFW ルールの管理に失敗しました:{{ .detail }}。現在の UFW バージョンが 0.35 未満の場合は、UFW をアップグレードしてから再試行してください'
Firewall: 'ファイアウォール'
FirewallTaskHost: 'ホストファイアウォール'
FirewallTaskForwarding: 'ポート転送'
FirewallTaskDocker: 'コンテナポート保護'
FirewallRuleTaskCreate: 'ファイアウォールルールを作成[{{ .name }}]'
FirewallRuleTaskUpdate: 'ファイアウォールルールを更新[{{ .name }}]'
FirewallRuleTaskDelete: 'ファイアウォールルールを削除[{{ .name }}]'
FirewallRuleTaskSync: 'ファイアウォールルールを同期[{{ .name }}]'
FirewallTaskInitialize: 'ファイアウォールを初期化[{{ .name }}]'
FirewallCreateRulesStep: 'ファイアウォールルールを作成'
FirewallCreateRulesResult: '作成結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件'
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件'
FirewallCreateRuleSkipped: '未実行'
FirewallCreateBatchStep: '{{ .backend }} に {{ .count }} 件のルールを一括送信'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }};{{ .count }} 件のルールに変換'
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません'
FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました'
FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました'
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件、一致済みで変更不要 {{ .unchanged }} 件'
FirewallSyncRuleUnchanged: '一致済み、変更不要'
FirewallSyncStep: '{{ .name }} にルールを同期'
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'
FirewallResetSourceResult: '移行元ファイアウォールをリセットし、データベースルール {{ .removed }} 件を削除しました'
FirewallInitializeChainsStep: '{{ .name }} のベースチェーンを初期化してバインド'
FirewallRestoreRulesStep: 'データベースルールを {{ .name }} に復元'
FirewallSyncWhitelistStep: 'ファイアウォールポート許可リストを同期'
FirewallEnableForwardingStep: 'ポート転送チェーンを有効化してバインド'
FirewallRestoreForwardingRulesStep: 'データベースのポート転送ルールを復元'
FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエンドと保護ポリシーを確認'
FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド'
FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください"
FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: 'Docker Compose 구성 출력에서 프로젝트 이
ErrComposeProjectNameEmpty: 'Docker Compose 구성 또는 파일의 상위 디렉터리에서 프로젝트 이름을 확인할 수 없습니다. 이름을 입력한 후 다시 시도하세요.'
ErrComposeNameInvalid: 'Compose 프로젝트 이름 형식이 잘못되었습니다. 소문자 또는 숫자로 시작하고 소문자, 숫자, 하이픈, 밑줄만 사용하여 1~256자로 입력하세요.'
ErrAppVersionUnavailable: '현재 애플리케이션 버전이 원격 서비스에서 제거되었습니다. 다른 버전을 선택한 후 다시 시도해 주세요.'
ErrVllmGB10ProOnly: '이 vLLM 버전은 1Panel 프로페셔널 에디션에서만 사용할 수 있습니다.'
ErrAppWarn: '비정상적인 상태입니다. 로그를 확인해 주세요.'
ErrAppParamKey: '매개변수 {{ .name }} 필드가 비정상입니다.'
ErrAppUpgrade: '애플리케이션 {{ .name }} 업그레이드에 실패했습니다 {{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: '{{ .name }} 도메인 이름 형식이 올바르지 않습니
ErrDefaultAlias: '기본값은 예약된 코드입니다. 다른 코드를 사용하세요'
ErrParentWebsite: '먼저 하위 사이트 {{ .name }}을 삭제해야 합니다.'
ErrBuildDirNotFound: '빌드 디렉토리가 존재하지 않습니다'
ErrBrotliDisabled: 'brotli 모듈이 활성화되지 않았습니다. 먼저 활성화하고 빌드하세요'
ErrBrotliUnsupported: '패널이 brotli 설정을 nginx.conf에 자동으로 연결할 수 없습니다. 파일을 확인하거나 OpenResty를 업그레이드하세요'
ErrModuleBuildUnsupported: '현재 OpenResty 버전은 모듈을 빌드할 수 없습니다. 먼저 업그레이드하세요'
ErrImageNotExist: '운영 환경 {{ .name }} 이미지가 존재하지 않습니다. 운영 환경을 다시 편집하세요.'
ErrProxyIsUsed: '로드 밸런싱이 역방향 프록시에 의해 사용되었으므로 삭제할 수 없습니다'
ErrSSLValid: '인증서 파일에 문제가 있습니다. 인증서 상태를 확인하세요'
@@ -506,6 +502,16 @@ Container: '컨테이너'
AppLink: '연결된 애플리케이션'
EnableSSL: 'HTTPS 활성화'
AppStore: '앱스토어'
Firewall: '방화벽'
FirewallSyncStep: '{{ .name }}에 규칙 동기화'
FirewallSyncResult: '동기화 결과: 성공 {{ .succeeded }}개, 기존 {{ .existing }}개, 실패 {{ .failed }}개'
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
FirewallSyncFailedResetSkipped: '규칙 {{ .failed }}개를 동기화하지 못해 원본 방화벽을 초기화하지 않았습니다'
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'
FirewallResetSourceResult: '원본 방화벽을 초기화하고 데이터베이스 규칙 {{ .removed }}개를 삭제했습니다'
FirewallVerifyTargetStep: '대상 방화벽 규칙 확인'
FirewallTargetRuleIneffective: '대상 규칙이 더 이상 적용되지 않습니다'
FirewallVerifyRuleFailed: '동기화된 규칙 {{ .name }} 확인 실패: {{ .detail }}'
TaskSync: '동기화'
LocalApp: '로컬 애플리케이션'
SubTask: '하위 작업'
@@ -661,7 +667,6 @@ XfsNotFound: 'xfs 파일 시스템이 감지되지 않았습니다, 먼저 xfspr
# 터미널
TerminalAIBlockedRiskyCommand: '위험한 명령이 차단되었습니다: {{ .command }}'
TerminalAIThinking: 'AI가 생각 중입니다...'
TerminalOutputTruncated: '[출력이 잘렸습니다. 최근 출력만 표시합니다]'
TerminalAIReadyToExecute: '생각이 완료되었습니다. Enter를 눌러 실행하세요 (소요 시간 {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'AI 요청 실패: {{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요'
ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 방화벽 체인을 찾을 수 없습니다. 현재 Docker 버전이 nftables 방화벽 백엔드를 지원하는지 확인하고 Docker를 다시 시작한 후 재시도하세요'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD 기본 정책이 DROP입니다. 정책을 조정한 후 다시 시도하세요'
ErrUFWRuleAdopt: 'UFW 규칙 관리에 실패했습니다: {{ .detail }}. 현재 UFW 버전이 0.35 미만이면 UFW를 업그레이드한 후 다시 시도하세요'
Firewall: '방화벽'
FirewallTaskHost: '호스트 방화벽'
FirewallTaskForwarding: '포트 포워딩'
FirewallTaskDocker: '컨테이너 포트 보호'
FirewallRuleTaskCreate: '방화벽 규칙 생성 [{{ .name }}]'
FirewallRuleTaskUpdate: '방화벽 규칙 업데이트 [{{ .name }}]'
FirewallRuleTaskDelete: '방화벽 규칙 삭제 [{{ .name }}]'
FirewallRuleTaskSync: '방화벽 규칙 동기화 [{{ .name }}]'
FirewallTaskInitialize: '방화벽 초기화 [{{ .name }}]'
FirewallCreateRulesStep: '방화벽 규칙 생성'
FirewallCreateRulesResult: '생성 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개'
FirewallRuleOperationResult: '작업 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개'
FirewallCreateRuleSkipped: '미실행'
FirewallCreateBatchStep: '{{ .backend }}에 규칙 {{ .count }}개 일괄 제출'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; 규칙 {{ .count }}개로 변환'
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다'
FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다'
FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다'
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개'
FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음'
FirewallSyncStep: '{{ .name }}에 규칙 동기화'
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'
FirewallResetSourceResult: '원본 방화벽을 초기화하고 데이터베이스 규칙 {{ .removed }}개를 삭제했습니다'
FirewallInitializeChainsStep: '{{ .name }} 기본 체인 초기화 및 바인딩'
FirewallRestoreRulesStep: '데이터베이스 규칙을 {{ .name }}에 복원'
FirewallSyncWhitelistStep: '방화벽 포트 허용 목록 동기화'
FirewallEnableForwardingStep: '포트 전달 체인 활성화 및 바인딩'
FirewallRestoreForwardingRulesStep: '데이터베이스 포트 전달 규칙 복원'
FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책 확인'
FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩'
FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요"
FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다"
+10 -46
View File
@@ -129,7 +129,6 @@ ErrComposeProjectNameParse: 'ບໍ່ສາມາດວິເຄາະຊື່
ErrComposeProjectNameEmpty: 'ບໍ່ສາມາດກຳນົດຊື່ໂຄງການຈາກການຕັ້ງຄ່າ Docker Compose ຫຼື ໂຟນເດີແມ່ຂອງໄຟລ໌ໄດ້. ກະລຸນາປ້ອນຊື່ແລ້ວລອງໃໝ່.'
ErrComposeNameInvalid: 'ຊື່ໂຄງການ Compose ບໍ່ຖືກຕ້ອງ: ຕ້ອງເລີ່ມດ້ວຍຕົວພິມນ້ອຍ ຫຼື ຕົວເລກ, ໃຊ້ໄດ້ສະເພາະຕົວພິມນ້ອຍ, ຕົວເລກ, ຂີດກາງ ແລະ ຂີດລຸ່ມ, ຄວາມຍາວ 1-256 ຕົວອັກສອນ.'
ErrAppVersionUnavailable: 'ເວີຊັນແອັບນີ້ຖືກລຶບອອກຈາກບໍລິການທາງໄກແລ້ວ ກະລຸນາເລືອກເວີຊັນອື່ນແລ້ວລອງໃໝ່'
ErrVllmGB10ProOnly: 'ເວີຊັນ vLLM ນີ້ມີໃຫ້ໃຊ້ສະເພາະໃນ 1Panel Professional Edition ເທົ່ານັ້ນ.'
ErrAppWarn: 'ສະຖານະແອັບຜິດປົກກະຕິ; ກະລຸນາກວດສອບບັນທຶກ'
ErrAppParamKey: 'ພາຣາມິເຕີຂອງແອັບບໍ່ຖືກຕ້ອງ: {{ .name }}'
ErrAppUpgrade: 'ອັບເກຣດແອັບລົ້ມເຫຼວ: {{ .name }} {{ .err }}'
@@ -206,9 +205,6 @@ ErrDomainFormat: 'ຮູບແບບໂດເມນບໍ່ຖືກຕ້ອ
ErrDefaultAlias: 'ຊື່ default ຖືກສະຫງວນໄວ້; ກະລຸນາໃຊ້ຊື່ອື່ນ'
ErrParentWebsite: 'ກະລຸນາລຶບເວັບໄຊຍ່ອຍ {{ .name }} ກ່ອນ'
ErrBuildDirNotFound: 'ບໍ່ມີໂຟນເດີ Build ຢູ່'
ErrBrotliDisabled: 'ໂມດູນ brotli ຍັງບໍ່ໄດ້ເປີດໃຊ້, ກະລຸນາເປີດໃຊ້ ແລະ ສ້າງກ່ອນ'
ErrBrotliUnsupported: 'ແຜງຄວບຄຸມບໍ່ສາມາດເຊື່ອມການຕັ້ງຄ່າ brotli ເຂົ້າ nginx.conf ໂດຍອັດຕະໂນມັດໄດ້; ກວດເບິ່ງໄຟລ໌ ຫຼື ອັບເກຣດ OpenResty'
ErrModuleBuildUnsupported: 'ລຸ້ນ OpenResty ນີ້ບໍ່ສາມາດສ້າງໂມດູນໄດ້, ກະລຸນາອັບເກຣດກ່ອນ'
ErrImageNotExist: 'ບໍ່ພົບຮູບພາບ runtime: {{ .name }}'
ErrProxyIsUsed: 'ຕົວຈັດການການໂຫຼດ (Load balancer) ຖືກໃຊ້ງານໂດຍ reverse proxy'
ErrSSLValid: 'ໄຟລ໌ໃບຮັບຮອງບໍ່ຖືກຕ້ອງ'
@@ -496,6 +492,16 @@ Container: 'ຄອນເທນເນີ'
AppLink: 'ແອັບພລິເຄຊັນທີ່ເຊື່ອມໂຍງ'
EnableSSL: 'ເປີດໃຊ້ HTTPS'
AppStore: 'ຮ້ານແອັບ'
Firewall: 'ໄຟວອລ'
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
FirewallSyncResult: 'ຜົນການຊິງຄ໌: ສຳເລັດ {{ .succeeded }}, ມີແລ້ວ {{ .existing }}, ລົ້ມເຫຼວ {{ .failed }}'
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
FirewallSyncFailedResetSkipped: 'ຊິງຄ໌ກົດ {{ .failed }} ລາຍການບໍ່ສຳເລັດ; ບໍ່ໄດ້ຣີເຊັດໄຟວອລຕົ້ນທາງ'
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'
FirewallResetSourceResult: 'ຣີເຊັດໄຟວອລຕົ້ນທາງແລ້ວ ແລະ ລຶບກົດຖານຂໍ້ມູນ {{ .removed }} ລາຍການ'
FirewallVerifyTargetStep: 'ກວດສອບກົດໄຟວອລປາຍທາງ'
FirewallTargetRuleIneffective: 'ກົດປາຍທາງບໍ່ມີຜົນແລ້ວ'
FirewallVerifyRuleFailed: 'ກວດສອບກົດທີ່ຊິງຄ໌ {{ .name }} ບໍ່ສຳເລັດ: {{ .detail }}'
TaskSync: 'ຊິງຄ໌'
LocalApp: 'ແອັບພລິເຄຊັນພາຍໃນເຄື່ອງ'
SubTask: 'ວຽກຍ່ອຍ'
@@ -651,7 +657,6 @@ XfsNotFound: 'ບໍ່ພົບ xfs; ກະລຸນາຕິດຕັ້ງ xf
# terminal
TerminalAIBlockedRiskyCommand: 'ບລັອກຄຳສັ່ງທີ່ມີຄວາມສ່ຽງ: {{ .command }}'
TerminalAIThinking: 'AI ກຳລັງຄິດ...'
TerminalOutputTruncated: '[ຜົນລັບຖືກຕັດ, ສະແດງສະເພາະຜົນລັບຫຼ້າສຸດ]'
TerminalAIReadyToExecute: 'ຄິດສຳເລັດແລ້ວ, ກົດ Enter ເພື່ອປະຕິບັດ (ໃຊ້ເວລາ: {{ .duration }}, ໂທເຄັນ: {{ .tokens }})'
TerminalAIRequestFailed: 'ຄຳຮ້ອງຂໍ AI ລົ້ມເຫຼວ: {{ .err }}'
FileAISearchEmptyDir: 'ບໍ່ພົບໄຟລ໌ ຫຼື ໂຟນເດີພາຍໃຕ້ເສັ້ນທາງນີ້ (ຫຼື ລາຍການທັງໝົດຖືກກັ່ນກອງອອກ).'
@@ -669,44 +674,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
ErrDockerNftablesChainUnavailable: 'ບໍ່ພົບ chain ໄຟວໍ nftables IPv4 ຂອງ Docker. ກະລຸນາກວດສອບວ່າ Docker ລຸ້ນປັດຈຸບັນຮອງຮັບ backend ໄຟວໍ nftables, ເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
ErrDockerForwardPolicyDrop: 'ນະໂຍບາຍເລີ່ມຕົ້ນ FORWARD ຂອງ {{ .family }} ແມ່ນ DROP. ກະລຸນາປັບແລ້ວລອງອີກຄັ້ງ'
ErrUFWRuleAdopt: 'ຈັດການກົດ UFW ບໍ່ສຳເລັດ: {{ .detail }}. ຖ້າ UFW ລຸ້ນປັດຈຸບັນຕ່ຳກວ່າ 0.35, ກະລຸນາອັບເກຣດ UFW ແລ້ວລອງໃໝ່'
Firewall: 'ໄຟວອລ'
FirewallTaskHost: 'ໄຟວໍໂຮສ'
FirewallTaskForwarding: 'ການສົ່ງຕໍ່ພອດ'
FirewallTaskDocker: 'ການປ້ອງກັນພອດຄອນເທນເນີ'
FirewallRuleTaskCreate: 'ສ້າງກົດໄຟວໍ [{{ .name }}]'
FirewallRuleTaskUpdate: 'ອັບເດດກົດໄຟວໍ [{{ .name }}]'
FirewallRuleTaskDelete: 'ລຶບກົດໄຟວໍ [{{ .name }}]'
FirewallRuleTaskSync: 'ຊິງຄ໌ກົດໄຟວໍ [{{ .name }}]'
FirewallTaskInitialize: 'ເລີ່ມຕົ້ນໄຟວໍ [{{ .name }}]'
FirewallCreateRulesStep: 'ສ້າງກົດໄຟວໍ'
FirewallCreateRulesResult: 'ຜົນການສ້າງ: ສຳເລັດ {{ .succeeded }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}'
FirewallRuleOperationResult: 'ຜົນການດຳເນີນການ: ສຳເລັດ {{ .succeeded }} ລາຍການ, ລົ້ມເຫຼວ {{ .failed }} ລາຍການ'
FirewallCreateRuleSkipped: 'ບໍ່ໄດ້ດຳເນີນການ'
FirewallCreateBatchStep: 'ສົ່ງ {{ .count }} ກົດເປັນຊຸດໄປຫາ {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ແປງເປັນ {{ .count }} ກົດ'
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ'
FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ'
FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້'
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)'
FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ'
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'
FirewallResetSourceResult: 'ຣີເຊັດໄຟວອລຕົ້ນທາງແລ້ວ ແລະ ລຶບກົດຖານຂໍ້ມູນ {{ .removed }} ລາຍການ'
FirewallInitializeChainsStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ພື້ນຖານ {{ .name }}'
FirewallRestoreRulesStep: 'ກູ້ຄືນກົດຖານຂໍ້ມູນໄປຫາ {{ .name }}'
FirewallSyncWhitelistStep: 'ຊິງຄ໌ລາຍການພອດໄຟວໍທີ່ອະນຸຍາດ'
FirewallEnableForwardingStep: 'ເປີດໃຊ້ ແລະ ຜູກ chain ສົ່ງຕໍ່ພອດ'
FirewallRestoreForwardingRulesStep: 'ກູ້ຄືນກົດສົ່ງຕໍ່ພອດຈາກຖານຂໍ້ມູນ'
FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແລະ ນະໂຍບາຍ'
FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}'
FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker'
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ"
FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: 'Gagal menghurai nama projek daripada output konfigu
ErrComposeProjectNameEmpty: 'Nama projek tidak dapat diperoleh daripada konfigurasi Docker Compose atau direktori induk fail. Masukkan nama dan cuba lagi.'
ErrComposeNameInvalid: 'Nama projek Compose tidak sah: mesti bermula dengan huruf kecil atau nombor, hanya mengandungi huruf kecil, nombor, tanda sempang dan garis bawah, serta sepanjang 1-256 aksara.'
ErrAppVersionUnavailable: 'Versi aplikasi ini telah dialih keluar daripada perkhidmatan jauh. Sila pilih versi lain dan cuba lagi.'
ErrVllmGB10ProOnly: 'Versi vLLM ini hanya tersedia dalam 1Panel Edisi Profesional.'
ErrAppWarn: 'Status tidak normal, sila semak log'
ErrAppParamKey: 'Parameter {{ .name }} medan tidak normal'
ErrAppUpgrade: 'Peningkatan {{ .name }} aplikasi gagal {{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: 'Format nama domain {{ .name }} tidak betul'
ErrDefaultAlias: 'lalai ialah kod simpanan, sila gunakan kod lain'
ErrParentWebsite: 'Anda perlu memadamkan subtapak {{ .name }} dahulu'
ErrBuildDirNotFound: 'Direktori binaan tidak wujud'
ErrBrotliDisabled: 'Modul brotli tidak diaktifkan, aktifkan dan bina dahulu'
ErrBrotliUnsupported: 'Panel tidak dapat menyambungkan tetapan brotli ke nginx.conf secara automatik; semak fail atau naik taraf OpenResty'
ErrModuleBuildUnsupported: 'Versi OpenResty ini tidak boleh membina modul, naik taraf dahulu'
ErrImageNotExist: 'Imej persekitaran operasi {{ .name }} tidak wujud, sila edit semula persekitaran pengendalian'
ErrProxyIsUsed: 'Pengimbang beban telah digunakan oleh pengganti terbalik, tidak boleh dipadamkan'
ErrSSLValid: 'Fail sijil bermasalah, sila periksa status sijil'
@@ -506,6 +502,16 @@ Container: 'Bekas'
AppLink: 'Aplikasi Terpaut'
EnableSSL: 'Dayakan HTTPS'
AppStore: 'Kedai aplikasi'
Firewall: 'Firewall'
FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}'
FirewallSyncResult: 'Hasil penyegerakan: {{ .succeeded }} berjaya, {{ .existing }} sedia ada, {{ .failed }} gagal'
FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan'
FirewallSyncFailedResetSkipped: '{{ .failed }} peraturan gagal disegerakkan; firewall sumber tidak ditetapkan semula'
FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}'
FirewallResetSourceResult: 'Firewall sumber ditetapkan semula; {{ .removed }} peraturan pangkalan data dipadamkan'
FirewallVerifyTargetStep: 'Sahkan peraturan firewall sasaran'
FirewallTargetRuleIneffective: 'Peraturan sasaran tidak lagi berkesan'
FirewallVerifyRuleFailed: 'Gagal mengesahkan peraturan disegerakkan {{ .name }}: {{ .detail }}'
TaskSync: 'Segerakkan'
LocalApp: 'Aplikasi Tempatan'
SubTask: 'Subtugas'
@@ -661,7 +667,6 @@ XfsNotFound: 'Sistem fail xfs tidak dikesan, sila pasang xfsprogs terlebih dahul
# terminal
TerminalAIBlockedRiskyCommand: 'Perintah berisiko telah disekat: {{ .command }}'
TerminalAIThinking: 'AI sedang berfikir...'
TerminalOutputTruncated: '[output dipotong, hanya output terkini dipaparkan]'
TerminalAIReadyToExecute: 'Pemikiran selesai, tekan Enter untuk jalankan (tempoh: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'Permintaan AI gagal: {{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Rantaian DOCKER-USER Docker tidak ditemui. Mulakan semula Docker dan cuba lagi'
ErrDockerNftablesChainUnavailable: 'Rantaian firewall IPv4 nftables Docker tidak ditemui. Pastikan versi Docker semasa menyokong bahagian belakang firewall nftables, mulakan semula Docker dan cuba lagi'
ErrDockerForwardPolicyDrop: 'Dasar lalai FORWARD {{ .family }} ialah DROP. Laraskan dasar dan cuba lagi'
ErrUFWRuleAdopt: 'Gagal mengurus peraturan UFW: {{ .detail }}. Jika versi UFW semasa lebih lama daripada 0.35, tingkatkan UFW dan cuba lagi'
Firewall: 'Firewall'
FirewallTaskHost: 'Tembok api hos'
FirewallTaskForwarding: 'Pemajuan port'
FirewallTaskDocker: 'Perlindungan port kontena'
FirewallRuleTaskCreate: 'Cipta peraturan tembok api [{{ .name }}]'
FirewallRuleTaskUpdate: 'Kemas kini peraturan tembok api [{{ .name }}]'
FirewallRuleTaskDelete: 'Padam peraturan tembok api [{{ .name }}]'
FirewallRuleTaskSync: 'Segerakkan peraturan tembok api [{{ .name }}]'
FirewallTaskInitialize: 'Mulakan tembok api [{{ .name }}]'
FirewallCreateRulesStep: 'Cipta peraturan tembok api'
FirewallCreateRulesResult: 'Hasil penciptaan: {{ .succeeded }} berjaya, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan'
FirewallRuleOperationResult: 'Hasil operasi: {{ .succeeded }} berjaya, {{ .failed }} gagal'
FirewallCreateRuleSkipped: 'Tidak dilaksanakan'
FirewallCreateBatchStep: 'Hantar {{ .count }} peraturan secara kelompok ke {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ditukar kepada {{ .count }} peraturan'
FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Tiada rekod disimpan dalam pangkalan data dan arahan yang dilaksanakan tidak dibatalkan'
FirewallCreateRulePersistenceFailed: 'Peraturan telah dicipta, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallAdoptRulePersistenceFailed: 'Arahan pengambilalihan peraturan telah dilaksanakan, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan, {{ .unchanged }} sudah sepadan (tiada perubahan diperlukan)'
FirewallSyncRuleUnchanged: 'Sudah sepadan; tiada perubahan diperlukan'
FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}'
FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan'
FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}'
FirewallResetSourceResult: 'Firewall sumber ditetapkan semula; {{ .removed }} peraturan pangkalan data dipadamkan'
FirewallInitializeChainsStep: 'Mulakan dan ikat rantai asas {{ .name }}'
FirewallRestoreRulesStep: 'Pulihkan peraturan pangkalan data ke {{ .name }}'
FirewallSyncWhitelistStep: 'Segerakkan senarai putih port firewall'
FirewallEnableForwardingStep: 'Aktifkan dan ikat rantai pemajuan port'
FirewallRestoreForwardingRulesStep: 'Pulihkan peraturan pemajuan port pangkalan data'
FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi'
FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}'
FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu."
FirewallWhitelistReleased: "{{ .name }}: perlindungan senarai dibenarkan telah dilepaskan; peraturan izin dikekalkan. Untuk menutup port, padamkan peraturan secara manual daripada senarai peraturan"
FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: 'Falha ao analisar o nome do projeto na saída de co
ErrComposeProjectNameEmpty: 'Não foi possível derivar um nome de projeto da configuração do Docker Compose nem do diretório pai do arquivo. Informe um nome e tente novamente.'
ErrComposeNameInvalid: 'Nome de projeto Compose inválido: deve começar com uma letra minúscula ou número, conter apenas letras minúsculas, números, hífens e sublinhados e ter de 1 a 256 caracteres.'
ErrAppVersionUnavailable: 'Esta versão do aplicativo foi removida do serviço remoto. Selecione outra versão e tente novamente.'
ErrVllmGB10ProOnly: 'Esta versão do vLLM está disponível apenas no 1Panel Professional Edition.'
ErrAppWarn: 'Status anormal, verifique o log'
ErrAppParamKey: 'O campo de parâmetro {{ .name }} está anormal'
ErrAppUpgrade: 'Falha na atualização do aplicativo {{ .name }} {{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: 'o formato do nome de domínio {{ .name }} está incorreto'
ErrDefaultAlias: 'padrão é um código reservado, use outro código'
ErrParentWebsite: 'Você precisa excluir o subsite {{ .name }} primeiro'
ErrBuildDirNotFound: 'O diretório de compilação não existe'
ErrBrotliDisabled: 'O módulo brotli não está ativado, ative-o e compile-o primeiro'
ErrBrotliUnsupported: 'O painel não conseguiu conectar as configurações brotli ao nginx.conf automaticamente; verifique o arquivo ou atualize o OpenResty'
ErrModuleBuildUnsupported: 'Esta versão do OpenResty não pode compilar módulos, atualize-a primeiro'
ErrImageNotExist: 'A imagem do ambiente operacional {{ .name }} não existe, edite novamente o ambiente operacional'
ErrProxyIsUsed: 'Balanceamento de carga foi usado por proxy reverso, não pode ser excluído'
ErrSSLValid: 'O arquivo do certificado está anormal, verifique o status do certificado'
@@ -506,6 +502,16 @@ Container: 'Recipiente'
AppLink: 'Aplicativo vinculado'
EnableSSL: 'Habilitar HTTPS'
AppStore: 'Loja de aplicativos'
Firewall: 'Firewall'
FirewallSyncStep: 'Sincronizar regras com {{ .name }}'
FirewallSyncResult: 'Resultado da sincronização: {{ .succeeded }} com sucesso, {{ .existing }} existentes, {{ .failed }} com falha'
FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização'
FirewallSyncFailedResetSkipped: '{{ .failed }} regras falharam na sincronização; o firewall de origem não foi redefinido'
FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}'
FirewallResetSourceResult: 'Firewall de origem redefinido; {{ .removed }} regras do banco de dados foram excluídas'
FirewallVerifyTargetStep: 'Verificar regras do firewall de destino'
FirewallTargetRuleIneffective: 'A regra de destino não está mais efetiva'
FirewallVerifyRuleFailed: 'Falha ao verificar a regra sincronizada {{ .name }}: {{ .detail }}'
TaskSync: 'Sincronizar'
LocalApp: 'Aplicativo local'
SubTask: 'Subtarefa'
@@ -661,7 +667,6 @@ XfsNotFound: 'Sistema de arquivos xfs não detectado instale xfsprogs primeiro'
# terminal
TerminalAIBlockedRiskyCommand: 'Comando de risco bloqueado: {{ .command }}'
TerminalAIThinking: 'A IA está pensando...'
TerminalOutputTruncated: '[saída truncada, mostrando apenas a saída recente]'
TerminalAIReadyToExecute: 'Pensamento concluído, pressione Enter para executar (duração: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'Falha na solicitação de AI: {{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'A cadeia DOCKER-USER do Docker não foi encontrada. Reinicie o Docker e tente novamente'
ErrDockerNftablesChainUnavailable: 'A cadeia de firewall IPv4 do nftables do Docker não foi encontrada. Confirme se a versão atual do Docker oferece suporte ao backend de firewall nftables, reinicie o Docker e tente novamente'
ErrDockerForwardPolicyDrop: 'A política padrão de FORWARD para {{ .family }} é DROP. Ajuste-a e tente novamente'
ErrUFWRuleAdopt: 'Falha ao gerenciar a regra UFW: {{ .detail }}. Se a versão atual do UFW for anterior à 0.35, atualize o UFW e tente novamente'
Firewall: 'Firewall'
FirewallTaskHost: 'Firewall do host'
FirewallTaskForwarding: 'Encaminhamento de portas'
FirewallTaskDocker: 'Proteção de portas de contêineres'
FirewallRuleTaskCreate: 'Criar regras de firewall [{{ .name }}]'
FirewallRuleTaskUpdate: 'Atualizar regras de firewall [{{ .name }}]'
FirewallRuleTaskDelete: 'Excluir regras de firewall [{{ .name }}]'
FirewallRuleTaskSync: 'Sincronizar regras de firewall [{{ .name }}]'
FirewallTaskInitialize: 'Inicializar firewall [{{ .name }}]'
FirewallCreateRulesStep: 'Criar regras de firewall'
FirewallCreateRulesResult: 'Resultado da criação: {{ .succeeded }} com sucesso, {{ .failed }} com falha, {{ .skipped }} não executadas'
FirewallRuleOperationResult: 'Resultado da operação: {{ .succeeded }} com sucesso, {{ .failed }} com falha'
FirewallCreateRuleSkipped: 'Não executada'
FirewallCreateBatchStep: 'Enviar {{ .count }} regras em lote para {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido em {{ .count }} regras'
FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Nenhum registro foi salvo no banco de dados e os comandos executados não foram revertidos'
FirewallCreateRulePersistenceFailed: 'A regra foi criada, mas não foi possível salvar seu registro de gerenciamento'
FirewallAdoptRulePersistenceFailed: 'A regra foi adotada, mas não foi possível salvar seu registro de gerenciamento'
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas, {{ .unchanged }} já correspondem (sem alterações necessárias)'
FirewallSyncRuleUnchanged: 'Já corresponde; nenhuma alteração necessária'
FirewallSyncStep: 'Sincronizar regras com {{ .name }}'
FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização'
FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}'
FirewallResetSourceResult: 'Firewall de origem redefinido; {{ .removed }} regras do banco de dados foram excluídas'
FirewallInitializeChainsStep: 'Inicializar e vincular as cadeias base do {{ .name }}'
FirewallRestoreRulesStep: 'Restaurar regras do banco de dados para {{ .name }}'
FirewallSyncWhitelistStep: 'Sincronizar a lista de portas permitidas do firewall'
FirewallEnableForwardingStep: 'Habilitar e vincular as cadeias de encaminhamento de portas'
FirewallRestoreForwardingRulesStep: 'Restaurar regras de encaminhamento de portas do banco de dados'
FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as políticas'
FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}'
FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker'
ErrFirewallRuleScopeChange: "O firewall atual não permite alterar o escopo de uma regra (como a família de endereços IPv4/IPv6). Crie uma nova regra."
FirewallWhitelistReleased: "{{ .name }}: proteção da lista de permissões removida; regra de permissão mantida. Para fechar a porta, exclua a regra manualmente da lista"
FirewallWhitelistRequired: "{{ .name }}: protegido pelas regras de portas obrigatórias do sistema"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: 'Не удалось определить имя п
ErrComposeProjectNameEmpty: 'Не удалось получить имя проекта из конфигурации Docker Compose или родительского каталога файла. Введите имя и повторите попытку.'
ErrComposeNameInvalid: 'Недопустимое имя проекта Compose: оно должно начинаться со строчной буквы или цифры, содержать только строчные буквы, цифры, дефисы и подчёркивания и иметь длину от 1 до 256 символов.'
ErrAppVersionUnavailable: 'Эта версия приложения удалена из удаленного сервиса. Выберите другую версию и повторите попытку.'
ErrVllmGB10ProOnly: 'Эта версия vLLM доступна только в профессиональной редакции 1Panel.'
ErrAppWarn: 'Ненормальное состояние, проверьте журнал'
ErrAppParamKey: 'Поле параметра {{ .name }} ненормально'
ErrAppUpgrade: 'Обновление приложения {{ .name }} не удалось {{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: 'Неверный формат доменного имени {{
ErrDefaultAlias: 'по умолчанию зарезервирован код, используйте другой код'
ErrParentWebsite: 'Сначала вам необходимо удалить дочерний сайт {{ .name }}'
ErrBuildDirNotFound: 'Каталог сборки не существует'
ErrBrotliDisabled: 'Модуль brotli не включён, сначала включите и соберите его'
ErrBrotliUnsupported: 'Панель не смогла автоматически подключить настройки brotli к nginx.conf; проверьте файл или обновите OpenResty'
ErrModuleBuildUnsupported: 'Эта версия OpenResty не может собирать модули, сначала обновите её'
ErrImageNotExist: 'Образ операционной среды {{ .name }} не существует, пожалуйста, отредактируйте операционную среду заново'
ErrProxyIsUsed: 'Балансировка нагрузки используется обратным прокси, невозможно удалить'
ErrSSLValid: 'Файл сертификата аномален, проверьте статус сертификата'
@@ -506,6 +502,16 @@ Container: 'Контейнер'
AppLink: 'Связанное приложение'
EnableSSL: 'Включить HTTPS'
AppStore: 'Магазин приложений'
Firewall: 'Межсетевой экран'
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
FirewallSyncResult: 'Результат синхронизации: успешно — {{ .succeeded }}, уже существуют — {{ .existing }}, ошибок — {{ .failed }}'
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
FirewallSyncFailedResetSkipped: 'Не удалось синхронизировать правил: {{ .failed }}; исходный межсетевой экран не был сброшен'
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'
FirewallResetSourceResult: 'Исходный межсетевой экран сброшен; удалено правил из базы данных: {{ .removed }}'
FirewallVerifyTargetStep: 'Проверить правила целевого межсетевого экрана'
FirewallTargetRuleIneffective: 'Целевое правило больше не действует'
FirewallVerifyRuleFailed: 'Не удалось проверить синхронизированное правило {{ .name }}: {{ .detail }}'
TaskSync: 'Синхронизировать'
LocalApp: 'Локальное приложение'
SubTask: 'Подзадача'
@@ -661,7 +667,6 @@ XfsNotFound: 'Файловая система xfs не обнаружена, с
# терминал
TerminalAIBlockedRiskyCommand: 'Опасная команда заблокирована: {{ .command }}'
TerminalAIThinking: 'AI думает...'
TerminalOutputTruncated: '[вывод усечён, показан только недавний вывод]'
TerminalAIReadyToExecute: 'Обдумывание завершено, нажмите Enter для выполнения (время: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'Ошибка запроса AI: {{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку'
ErrDockerNftablesChainUnavailable: 'Цепочка IPv4 брандмауэра nftables Docker не найдена. Убедитесь, что текущая версия Docker поддерживает бэкенд брандмауэра nftables, перезапустите Docker и повторите попытку'
ErrDockerForwardPolicyDrop: 'Политика FORWARD по умолчанию для {{ .family }} — DROP. Измените её и повторите попытку'
ErrUFWRuleAdopt: 'Не удалось принять правило UFW под управление: {{ .detail }}. Если текущая версия UFW ниже 0.35, обновите UFW и повторите попытку'
Firewall: 'Межсетевой экран'
FirewallTaskHost: 'Межсетевой экран хоста'
FirewallTaskForwarding: 'Перенаправление портов'
FirewallTaskDocker: 'Защита портов контейнеров'
FirewallRuleTaskCreate: 'Создание правил межсетевого экрана [{{ .name }}]'
FirewallRuleTaskUpdate: 'Обновление правил межсетевого экрана [{{ .name }}]'
FirewallRuleTaskDelete: 'Удаление правил межсетевого экрана [{{ .name }}]'
FirewallRuleTaskSync: 'Синхронизация правил межсетевого экрана [{{ .name }}]'
FirewallTaskInitialize: 'Инициализация межсетевого экрана [{{ .name }}]'
FirewallCreateRulesStep: 'Создание правил межсетевого экрана'
FirewallCreateRulesResult: 'Результат создания: успешно — {{ .succeeded }}, ошибок — {{ .failed }}, не выполнено — {{ .skipped }}'
FirewallRuleOperationResult: 'Результат операции: успешно — {{ .succeeded }}, ошибок — {{ .failed }}'
FirewallCreateRuleSkipped: 'Не выполнено'
FirewallCreateBatchStep: 'Отправка {{ .count }} правил одним пакетом в {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; преобразовано в {{ .count }} правил'
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены'
FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления'
FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось'
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}, уже совпадают (изменения не нужны): {{ .unchanged }}'
FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны'
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'
FirewallResetSourceResult: 'Исходный межсетевой экран сброшен; удалено правил из базы данных: {{ .removed }}'
FirewallInitializeChainsStep: 'Инициализировать и привязать базовые цепочки {{ .name }}'
FirewallRestoreRulesStep: 'Восстановить правила базы данных в {{ .name }}'
FirewallSyncWhitelistStep: 'Синхронизировать белый список портов межсетевого экрана'
FirewallEnableForwardingStep: 'Включить и привязать цепочки перенаправления портов'
FirewallRestoreForwardingRulesStep: 'Восстановить правила перенаправления портов из базы данных'
FirewallInspectDockerGuardStep: 'Проверить бэкенд межсетевого экрана Docker и политики'
FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}'
FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker'
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил"
FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: 'Docker Compose yapılandırma çıktısından proje
ErrComposeProjectNameEmpty: 'Docker Compose yapılandırmasından veya dosyanın üst dizininden proje adı türetilemedi. Bir ad girip yeniden deneyin.'
ErrComposeNameInvalid: 'Geçersiz Compose proje adı: küçük harf veya rakamla başlamalı, yalnızca küçük harf, rakam, kısa çizgi ve alt çizgi içermeli ve 1-256 karakter uzunluğunda olmalıdır.'
ErrAppVersionUnavailable: 'Bu uygulama sürümü uzak hizmetten kaldırıldı. Lütfen başka bir sürüm seçip tekrar deneyin.'
ErrVllmGB10ProOnly: 'Bu vLLM sürümü yalnızca 1Panel Professional Edition sürümünde kullanılabilir.'
ErrAppWarn: 'Anormal durum, lütfen günlüğü kontrol edin'
ErrAppParamKey: 'Parametre {{ .name }} alanı anormal'
ErrAppUpgrade: 'Uygulama {{ .name }} yükseltmesi başarısız {{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: '{{ .name }} alan adı formatı yanlış'
ErrDefaultAlias: 'default ayrılmış bir kod, lütfen başka bir kod kullanın'
ErrParentWebsite: 'Önce {{ .name }} alt sitesini silmeniz gerekiyor'
ErrBuildDirNotFound: 'Yapı dizini mevcut değil'
ErrBrotliDisabled: 'brotli modülü etkin değil, önce etkinleştirin ve derleyin'
ErrBrotliUnsupported: 'Panel brotli ayarlarını nginx.conf dosyasına otomatik olarak bağlayamadı; dosyayı kontrol edin veya OpenResty yi yükseltin'
ErrModuleBuildUnsupported: 'Bu OpenResty sürümü modül derleyemez, önce yükseltin'
ErrImageNotExist: 'İşletim ortamı {{ .name }} image mevcut değil, lütfen işletim ortamını yeniden düzenleyin'
ErrProxyIsUsed: 'Yük dengeleme ters proxy tarafından kullanıldı, silinemez'
ErrSSLValid: 'Sertifika dosyası anormal, lütfen sertifika durumunu kontrol edin'
@@ -506,6 +502,16 @@ Container: 'Konteyner'
AppLink: 'Bağlantılı Uygulama'
EnableSSL: 'HTTPS Etkinleştir'
AppStore: 'Uygulama Mağazası'
Firewall: 'Güvenlik duvarı'
FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle'
FirewallSyncResult: 'Eşitleme sonucu: {{ .succeeded }} başarılı, {{ .existing }} mevcut, {{ .failed }} başarısız'
FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi'
FirewallSyncFailedResetSkipped: '{{ .failed }} kural eşitlenemedi; kaynak güvenlik duvarı sıfırlanmadı'
FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak'
FirewallResetSourceResult: 'Kaynak güvenlik duvarı sıfırlandı; {{ .removed }} veritabanı kuralı silindi'
FirewallVerifyTargetStep: 'Hedef güvenlik duvarı kurallarını doğrula'
FirewallTargetRuleIneffective: 'Hedef kural artık etkili değil'
FirewallVerifyRuleFailed: 'Eşitlenen {{ .name }} kuralı doğrulanamadı: {{ .detail }}'
TaskSync: 'Senkronize Et'
LocalApp: 'Yerel Uygulama'
SubTask: 'Alt görev'
@@ -661,7 +667,6 @@ XfsNotFound: 'XFS dosya sistemi algılanmadı, lütfen önce xfsprogs''i yükley
# terminal
TerminalAIBlockedRiskyCommand: 'Riskli komut engellendi: {{ .command }}'
TerminalAIThinking: 'AI dusunuyor...'
TerminalOutputTruncated: '[çıktı kısaltıldı, yalnızca son çıktı gösteriliyor]'
TerminalAIReadyToExecute: 'Dusunme tamamlandi, calistirmak icin Enter tusuna basin (sure: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'AI istegi basarisiz oldu: {{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
# Firewall
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER zinciri bulunamadı. Docker’ı yeniden başlatıp tekrar deneyin'
ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 güvenlik duvarı zinciri bulunamadı. Geçerli Docker sürümünün nftables güvenlik duvarı arka ucunu desteklediğini doğrulayın, Docker’ı yeniden başlatıp tekrar deneyin'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD varsayılan ilkesi DROP. İlkeyi düzenleyip tekrar deneyin'
ErrUFWRuleAdopt: 'UFW kuralı yönetilemedi: {{ .detail }}. Geçerli UFW sürümü 0.35’ten eskiyse UFW’yi yükseltip tekrar deneyin'
Firewall: 'Güvenlik duvarı'
FirewallTaskHost: 'Ana makine güvenlik duvarı'
FirewallTaskForwarding: 'Port yönlendirme'
FirewallTaskDocker: 'Konteyner port koruması'
FirewallRuleTaskCreate: 'Güvenlik duvarı kuralları oluştur [{{ .name }}]'
FirewallRuleTaskUpdate: 'Güvenlik duvarı kurallarını güncelle [{{ .name }}]'
FirewallRuleTaskDelete: 'Güvenlik duvarı kurallarını sil [{{ .name }}]'
FirewallRuleTaskSync: 'Güvenlik duvarı kurallarını eşitle [{{ .name }}]'
FirewallTaskInitialize: 'Güvenlik duvarını başlat [{{ .name }}]'
FirewallCreateRulesStep: 'Güvenlik duvarı kuralları oluştur'
FirewallCreateRulesResult: 'Oluşturma sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi'
FirewallRuleOperationResult: 'İşlem sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız'
FirewallCreateRuleSkipped: 'Yürütülmedi'
FirewallCreateBatchStep: '{{ .backend }} için {{ .count }} kuralı toplu gönder'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; {{ .count }} kurala dönüştürüldü'
FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Veritabanına kayıt yazılmadı ve yürütülen komutlar geri alınmadı'
FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi'
FirewallAdoptRulePersistenceFailed: 'Kuralı yönetime alma komutu yürütüldü, ancak yönetim kaydı kaydedilemedi'
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi, {{ .unchanged }} zaten eşleşiyor (değişiklik gerekmiyor)'
FirewallSyncRuleUnchanged: 'Zaten eşleşiyor; değişiklik gerekmiyor'
FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle'
FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi'
FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak'
FirewallResetSourceResult: 'Kaynak güvenlik duvarı sıfırlandı; {{ .removed }} veritabanı kuralı silindi'
FirewallInitializeChainsStep: '{{ .name }} temel zincirlerini başlat ve bağla'
FirewallRestoreRulesStep: 'Veritabanı kurallarını {{ .name }} üzerine geri yükle'
FirewallSyncWhitelistStep: 'Güvenlik duvarı bağlantı noktası izin listesini eşitle'
FirewallEnableForwardingStep: 'Bağlantı noktası yönlendirme zincirlerini etkinleştir ve bağla'
FirewallRestoreForwardingRulesStep: 'Veritabanı bağlantı noktası yönlendirme kurallarını geri yükle'
FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkelerini denetle'
FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla'
FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet'
ErrFirewallRuleScopeChange: "Mevcut güvenlik duvarı, kuralın kapsamını (IPv4/IPv6 adres ailesi gibi) değiştirmeyi desteklemiyor. Lütfen yeni bir kural oluşturun."
FirewallWhitelistReleased: "{{ .name }}: izin listesi koruması kaldırıldı; izin kuralı korundu. Portu kapatmak için kuralı kural listesinden elle silin"
FirewallWhitelistRequired: "{{ .name }}: zorunlu sistem portu kuralları tarafından korunuyor"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: '解析 Docker Compose 設定輸出中的專案名
ErrComposeProjectNameEmpty: '無法從 Docker Compose 設定或檔案父目錄中推導出專案名稱,請填寫名稱後重試'
ErrComposeNameInvalid: 'Compose 專案名稱格式錯誤:必須以小寫字母或數字開頭,僅支援小寫字母、數字、- 和 _,長度為 1-256 個字元'
ErrAppVersionUnavailable: '目前應用程式版本已從遠端服務下架,請選擇其他版本後重試'
ErrVllmGB10ProOnly: '此 vLLM 版本僅支援 1Panel 專業版'
ErrAppWarn: '狀態異常,請檢視日誌'
ErrAppParamKey: '參數{{ .name }} 欄位異常'
ErrAppUpgrade: '應用程式{{ .name }} 升級失敗{{ .err }}'
@@ -216,9 +215,6 @@ ErrDomainFormat: '{{ .name }} 網域格式不正確'
ErrDefaultAlias: 'default 為保留代號,請使用其他代號'
ErrParentWebsite: '需要先移除子網站{{ .name }}'
ErrBuildDirNotFound: '建置目錄不存在'
ErrBrotliDisabled: 'brotli 模組未啟用,請先啟用並建置'
ErrBrotliUnsupported: '面板無法自動將 brotli 設定寫入 nginx.conf,請檢查設定檔或升級 OpenResty'
ErrModuleBuildUnsupported: '目前 OpenResty 版本無法建置模組,請先升級'
ErrImageNotExist: '執行環境{{ .name }} 映像不存在,請重新編輯執行環境'
ErrProxyIsUsed: '負載均衡已被反向代理使用,無法刪除'
ErrSSLValid: '憑證檔案異常,請檢查憑證狀態!'
@@ -506,6 +502,16 @@ Container: '容器'
AppLink: '關聯應用程式'
EnableSSL: '開啟HTTPS'
AppStore: '應用程式商店'
Firewall: '防火牆'
FirewallSyncStep: '同步規則到 {{ .name }}'
FirewallSyncResult: '同步結果:成功 {{ .succeeded }} 條,已存在 {{ .existing }} 條,失敗 {{ .failed }} 條'
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
FirewallSyncFailedResetSkipped: '{{ .failed }} 條防火牆規則同步失敗,未重設來源防火牆'
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'
FirewallResetSourceResult: '來源防火牆重設完成,已刪除 {{ .removed }} 條資料庫規則'
FirewallVerifyTargetStep: '驗證目標防火牆規則'
FirewallTargetRuleIneffective: '目標規則已失效'
FirewallVerifyRuleFailed: '驗證同步規則 {{ .name }} 失敗:{{ .detail }}'
TaskSync: '同步'
LocalApp: '本機應用'
SubTask: '子任務'
@@ -661,7 +667,6 @@ XfsNotFound: '未偵測到 xfs 檔案系統,請先安裝 xfsprogs'
# 終端
TerminalAIBlockedRiskyCommand: '已攔截風險命令:{{ .command }}'
TerminalAIThinking: 'AI 正在思考...'
TerminalOutputTruncated: '[輸出已截斷,僅顯示最近輸出]'
TerminalAIReadyToExecute: '思考完成,請按 Enter 執行(耗時 {{ .duration }},token:{{ .tokens }})'
TerminalAIRequestFailed: 'AI 請求失敗:{{ .err }}'
@@ -678,44 +683,3 @@ AIProviderBailian: '阿里雲百煉'
AIProviderBailianCodingPlan: '阿里雲百煉 Coding Plan'
AIProviderArk: '火山方舟'
AIProviderArkCodingPlan: '火山方舟 Coding Plan'
# 防火牆
ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試'
ErrDockerNftablesChainUnavailable: '未偵測到 Docker 的 nftables IPv4 防火牆鏈,請確認目前 Docker 版本支援 nftables 防火牆後端,重新啟動 Docker 後再試'
ErrDockerForwardPolicyDrop: '偵測到 {{ .family }} FORWARD 預設策略為 DROP,請調整後重試'
ErrUFWRuleAdopt: 'UFW 規則納管失敗:{{ .detail }}。如果目前 UFW 版本低於 0.35,請先升級 UFW 後再試'
Firewall: '防火牆'
FirewallTaskHost: '主機防火牆'
FirewallTaskForwarding: '連接埠轉發'
FirewallTaskDocker: '容器連接埠防護'
FirewallRuleTaskCreate: '建立防火牆規則[{{ .name }}]'
FirewallRuleTaskUpdate: '更新防火牆規則[{{ .name }}]'
FirewallRuleTaskDelete: '刪除防火牆規則[{{ .name }}]'
FirewallRuleTaskSync: '同步防火牆規則[{{ .name }}]'
FirewallTaskInitialize: '初始化防火牆[{{ .name }}]'
FirewallCreateRulesStep: '建立防火牆規則'
FirewallCreateRulesResult: '建立結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條'
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條'
FirewallCreateRuleSkipped: '未執行'
FirewallCreateBatchStep: '向 {{ .backend }} 批次提交 {{ .count }} 條規則'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};轉換為 {{ .count }} 條規則'
FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復'
FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗'
FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗'
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條,已一致無需變更 {{ .unchanged }} 條'
FirewallSyncRuleUnchanged: '已一致,無需變更'
FirewallSyncStep: '同步規則到 {{ .name }}'
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'
FirewallResetSourceResult: '來源防火牆重設完成,已刪除 {{ .removed }} 條資料庫規則'
FirewallInitializeChainsStep: '初始化並綁定 {{ .name }} 基礎鏈'
FirewallRestoreRulesStep: '恢復資料庫規則至 {{ .name }}'
FirewallSyncWhitelistStep: '同步防火牆連接埠白名單'
FirewallEnableForwardingStep: '啟用並綁定連接埠轉發鏈'
FirewallRestoreForwardingRulesStep: '恢復資料庫連接埠轉發規則'
FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略'
FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈'
FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除"
FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護"
+10 -46
View File
@@ -139,7 +139,6 @@ ErrComposeProjectNameParse: "解析 Docker Compose 配置输出中的项目名
ErrComposeProjectNameEmpty: "无法从 Docker Compose 配置或文件父目录中推导出项目名称,请填写名称后重试"
ErrComposeNameInvalid: "Compose 项目名称格式错误:必须以小写字母或数字开头,仅支持小写字母、数字、- 和 _,长度为 1-256 个字符"
ErrAppVersionUnavailable: "当前应用版本已从远程服务下架,请选择其他版本后重试"
ErrVllmGB10ProOnly: "该 vLLM 版本仅支持 1Panel 专业版"
ErrAppWarn: "状态异常,请查看日志"
ErrAppParamKey: "参数 {{ .name }} 字段异常"
ErrAppUpgrade: "应用 {{ .name }} 升级失败 {{ .err }}"
@@ -216,9 +215,6 @@ ErrDomainFormat: "{{ .name }} 域名格式不正确"
ErrDefaultAlias: "default 为保留代号,请使用其他代号"
ErrParentWebsite: "需要先删除子网站 {{ .name }}"
ErrBuildDirNotFound: "构建目录不存在"
ErrBrotliDisabled: "brotli 模块未启用,请先启用并构建"
ErrBrotliUnsupported: "面板无法自动将 brotli 配置写入 nginx.conf,请检查配置文件或升级 OpenResty"
ErrModuleBuildUnsupported: "当前 OpenResty 版本无法构建模块,请先升级"
ErrImageNotExist: "运行环境 {{ .name }} 镜像不存在,请重新编辑运行环境"
ErrProxyIsUsed: "负载均衡已被反向代理使用,无法删除"
ErrSSLValid: '证书文件异常,请检查证书状态!'
@@ -506,6 +502,16 @@ Container: "容器"
AppLink: "关联应用"
EnableSSL: "开启 HTTPS"
AppStore: "应用商店"
Firewall: "防火墙"
FirewallSyncStep: "同步规则到 {{ .name }}"
FirewallSyncResult: "同步结果:成功 {{ .succeeded }} 条,已存在 {{ .existing }} 条,失败 {{ .failed }} 条"
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
FirewallSyncFailedResetSkipped: "{{ .failed }} 条防火墙规则同步失败,未重置源防火墙"
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"
FirewallResetSourceResult: "源防火墙重置完成,已删除 {{ .removed }} 条数据库规则"
FirewallVerifyTargetStep: "验证目标防火墙规则"
FirewallTargetRuleIneffective: "目标规则已失效"
FirewallVerifyRuleFailed: "验证同步规则 {{ .name }} 失败:{{ .detail }}"
TaskSync: "同步"
LocalApp: "本地应用"
SubTask: "子任务"
@@ -661,7 +667,6 @@ XfsNotFound: "未检测到 xfs 文件系统,请先安装 xfsprogs"
# 终端
TerminalAIBlockedRiskyCommand: "已拦截风险命令:{{ .command }}"
TerminalAIThinking: "AI 正在思考..."
TerminalOutputTruncated: "[输出已截断,仅显示最近输出]"
TerminalAIReadyToExecute: "思考完成,请回车执行(耗时 {{ .duration }},token:{{ .tokens }})"
TerminalAIRequestFailed: "AI 请求失败:{{ .err }}"
@@ -678,44 +683,3 @@ AIProviderBailian: "阿里云百炼"
AIProviderBailianCodingPlan: "阿里云百炼 Coding Plan"
AIProviderArk: "火山方舟"
AIProviderArkCodingPlan: "火山方舟 Coding Plan"
# 防火墙
ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试"
ErrDockerNftablesChainUnavailable: "未检测到 Docker 的 nftables IPv4 防火墙链,请确认当前 Docker 版本支持 nftables 防火墙后端,重启 Docker 后重试"
ErrDockerForwardPolicyDrop: '检测到 {{ .family }} FORWARD 默认策略为 DROP,请调整后重试'
ErrUFWRuleAdopt: "UFW 规则纳管失败:{{ .detail }}。如果当前 UFW 版本低于 0.35,请先升级 UFW 后重试"
Firewall: "防火墙"
FirewallTaskHost: '主机防火墙'
FirewallTaskForwarding: '端口转发'
FirewallTaskDocker: '容器端口防护'
FirewallRuleTaskCreate: '创建防火墙规则[{{ .name }}]'
FirewallRuleTaskUpdate: '更新防火墙规则[{{ .name }}]'
FirewallRuleTaskDelete: '删除防火墙规则[{{ .name }}]'
FirewallRuleTaskSync: '同步防火墙规则[{{ .name }}]'
FirewallTaskInitialize: '初始化防火墙[{{ .name }}]'
FirewallCreateRulesStep: '创建防火墙规则'
FirewallCreateRulesResult: '创建结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条'
FirewallRuleOperationResult: '操作结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条'
FirewallCreateRuleSkipped: '未执行'
FirewallCreateBatchStep: '向 {{ .backend }} 批量提交 {{ .count }} 条规则'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}:{{ .rule }};转换为 {{ .count }} 条规则'
FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚'
FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败'
FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败'
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条,已一致无需变更 {{ .unchanged }} 条'
FirewallSyncRuleUnchanged: '已一致,无需变更'
FirewallSyncStep: "同步规则到 {{ .name }}"
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"
FirewallResetSourceResult: "源防火墙重置完成,已删除 {{ .removed }} 条数据库规则"
FirewallInitializeChainsStep: "初始化并绑定 {{ .name }} 基础链"
FirewallRestoreRulesStep: "恢复数据库规则到 {{ .name }}"
FirewallSyncWhitelistStep: "同步防火墙端口白名单"
FirewallEnableForwardingStep: "启用并绑定端口转发链"
FirewallRestoreForwardingRulesStep: "恢复数据库端口转发规则"
FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略"
FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链"
FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除"
FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护"
+23 -30
View File
@@ -11,11 +11,11 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/init/migration/migrations"
migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
)
func Init() {
@@ -26,36 +26,25 @@ func Init() {
return
}
clientName := client.Name()
initialize := false
defer func() {
if err := migrations.TransferFirewalldSSHService(ctx, client, service.NewIFirewallService().SyncPortWhitelist); err != nil {
global.LOG.Warnf("synchronize firewall whitelist on startup failed, err: %v", err)
}
if initialize {
initDockerPortGuard(ctx)
}
}()
if err := migrationutils.TransferHostFirewall(ctx, clientName); err != nil {
global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err)
return
}
if err := migrationutils.TransferLegacyHostFirewallRuleOwnership(ctx, clientName, service.AdoptLegacyHostFirewallRuleOwnership); err != nil {
global.LOG.Warnf("transfer legacy host firewall rule ownership failed, err: %v", err)
}
if err := migrationutils.TransferFirewallForwarding(ctx); err != nil {
global.LOG.Errorf("transfer legacy forwarding rules failed, err: %v", err)
return
}
if err := initForwardingRules(ctx); err != nil {
global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err)
}
initialize = needInit()
if !initialize {
repairIptablesBaseChains(clientName)
if !needInit() {
repairIptablesIPv6BaseChains(clientName)
return
}
defer initDockerPortGuard(ctx)
InitPingStatus()
global.LOG.Info("initializing firewall settings...")
if err := initForwardingRules(ctx); err != nil {
global.LOG.Errorf("restore forwarding rules failed, err: %v", err)
return
}
if clientName == "nftables" {
if err := nftables_helper.Restore(); err != nil {
global.LOG.Errorf("restore nftables rules failed, err: %v", err)
@@ -73,12 +62,12 @@ func Init() {
return
}
settingRepo := repo.NewISettingRepo()
requiredPorts, err := service.LoadRequiredFirewallPortWhiteList()
if err != nil {
global.LOG.Errorf("load required firewall ports failed, err: %v", err)
panelPort := service.LoadPanelPort()
if len(panelPort) == 0 {
global.LOG.Errorf("find 1panel service port failed")
return
}
if err := iptables_helper.RestoreBaseChains(requiredPorts); err != nil {
if err := iptables_helper.RestoreBaseChains(panelPort); err != nil {
global.LOG.Errorf("restore iptables base chains failed, err: %v", err)
return
}
@@ -94,7 +83,7 @@ func Init() {
}
func repairIptablesBaseChains(clientName string) {
func repairIptablesIPv6BaseChains(clientName string) {
if clientName != constant.FirewallProviderIptables {
return
}
@@ -103,12 +92,16 @@ func repairIptablesBaseChains(clientName string) {
if status != constant.StatusEnable {
return
}
manager := iptables_helper.Manager{
LoadRequiredPorts: service.LoadRequiredFirewallPortWhiteList,
initialized, bound, err := iptables_helper.LoadFamilyInitStatus(constant.FirewallFamilyIPv6, "base")
if err == nil && initialized && bound {
return
}
if err := manager.RepairBaseChains(); err != nil {
global.LOG.Warnf("repair iptables base chains failed, err: %v", err)
panelPort := service.LoadPanelPort()
if err := iptables_helper.RepairIPv6BaseChains(panelPort); err != nil {
global.LOG.Warnf("repair IPv6 iptables base chains failed, err: %v", err)
return
}
global.LOG.Info("repaired IPv6 iptables base chains successfully")
}
func initDockerPortGuard(ctx context.Context) {
@@ -153,7 +146,7 @@ func needInit() bool {
func InitPingStatus() {
global.LOG.Info("initializing ban ping status from settings...")
status := firewall.LoadPingStatus()
status := ping.LoadStatus()
statusInDB, _ := repo.NewISettingRepo().GetValueByKey("BanPing")
if statusInDB == status {
return
@@ -163,7 +156,7 @@ func InitPingStatus() {
if statusInDB == constant.StatusDisable {
enable = "0"
}
if err := firewall.UpdatePingStatus(enable); err != nil {
if err := ping.UpdateStatus(enable); err != nil {
global.LOG.Errorf("initialize ping status failed: %v", err)
}
}
+6 -17
View File
@@ -5,7 +5,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/init/migration/migrations"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
func Init() {
@@ -109,8 +108,6 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.InitDockerPortGuardStatus,
migrations.NormalizeFirewallBackendSelections,
migrations.SimplifyFirewallRulePolicy,
migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources,
}
}
@@ -125,21 +122,13 @@ func InitTaskDB() {
}
func InitAlertDB() {
if err := migrateAlertDB(global.AlertDB); err != nil {
m := gormigrate.New(global.AlertDB, gormigrate.DefaultOptions, []*gormigrate.Migration{
migrations.MigrateAlertMethodConfigIDs,
migrations.MigrateAlertLogTaskMethodConfigIDs,
migrations.AddAlertAuditUser,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
panic(err)
}
}
func migrateAlertDB(db *gorm.DB) error {
options := *gormigrate.DefaultOptions
options.UseTransaction = true
m := gormigrate.New(db, &options, []*gormigrate.Migration{
migrations.AddAlertConfigUIDAndSecret,
migrations.MigrateAlertMethodConfigIDs,
migrations.MigrateAlertLogTaskMethodConfigIDs,
migrations.AddAlertAuditUser,
migrations.AddAlertTaskDeliveryLogID,
})
return m.Migrate()
}
@@ -1,215 +0,0 @@
package migrations
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"unicode"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const firewalldSSHServiceMigrationID = "20260916-remove-firewalld-ssh-service"
func TransferFirewalldSSHService(ctx context.Context, client lifecycle.Client, syncWhitelist func(context.Context) error) error {
return transferFirewalldSSHService(ctx, global.DB, client, syncWhitelist)
}
func transferFirewalldSSHService(ctx context.Context, db *gorm.DB, client lifecycle.Client, syncWhitelist func(context.Context) error) error {
if err := syncWhitelist(ctx); err != nil {
return err
}
if client.Name() != lifecycle.ProviderFirewalld {
return nil
}
var count int64
if err := db.WithContext(ctx).Table("migrations").Where("id = ?", firewalldSSHServiceMigrationID).Count(&count).Error; err != nil {
return fmt.Errorf("check firewalld SSH service migration: %w", err)
}
if count > 0 {
return nil
}
active, err := client.Status()
if err != nil || !active {
return err
}
if err := ctx.Err(); err != nil {
return err
}
if err := providers.RemoveFirewalldSSHService(); err != nil {
return fmt.Errorf("transfer firewalld SSH access to whitelist: %w", err)
}
if err := db.WithContext(ctx).Table("migrations").Clauses(clause.OnConflict{DoNothing: true}).
Create(map[string]interface{}{"id": firewalldSSHServiceMigrationID}).Error; err != nil {
return fmt.Errorf("record firewalld SSH service migration: %w", err)
}
return nil
}
var MigrateFirewallPortWhitelistSources = &gormigrate.Migration{
ID: "20260915-migrate-firewall-port-whitelist-sources",
Migrate: func(tx *gorm.DB) error {
var setting model.Setting
err := tx.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
rules, err := migrateFirewallPortWhitelist(setting.Value)
if err != nil {
return fmt.Errorf("migrate firewall port whitelist: %w", err)
}
value, err := json.Marshal(rules)
if err != nil {
return err
}
if setting.ID == 0 {
err = tx.Create(&model.Setting{Key: constant.FirewallPortWhiteList, Value: string(value)}).Error
} else {
err = tx.Model(&setting).Update("value", string(value)).Error
}
if err != nil {
return err
}
return tx.Where("key = ?", "FirewallPortWhiteListPending").Delete(&model.Setting{}).Error
},
}
type legacyPortWhitelist struct {
Ports []string `json:"ports"`
Family string `json:"family"`
Port string `json:"port"`
Protocol string `json:"protocol"`
Type string `json:"type"`
Sources []string `json:"sources"`
}
func (entry legacyPortWhitelist) singlePortRule() firewall.PortWhitelist {
rule := firewall.PortWhitelist{Port: entry.Port, Protocol: entry.Protocol, Type: entry.Type, Sources: entry.Sources}
if strings.TrimSpace(rule.Type) != "" && rule.Port == "" && len(entry.Ports) > 0 {
rule.Port = entry.Ports[0]
}
return rule
}
func migrateFirewallPortWhitelist(value string) ([]firewall.PortWhitelist, error) {
legacy, err := parseLegacyPortWhitelist(value)
if err != nil {
return nil, err
}
rules := make([]firewall.PortWhitelist, 0, len(legacy)+5)
indexes := make(map[string]int)
key := func(rule firewall.PortWhitelist) string {
if rule.Type != "" {
return rule.Type + "/" + rule.Protocol
}
return rule.Type + "/" + rule.Protocol + "/" + rule.Port
}
for index, entry := range legacy {
family := strings.ToLower(strings.TrimSpace(entry.Family))
if family != "" && family != constant.FirewallFamilyIPv4 && family != constant.FirewallFamilyIPv6 {
return nil, fmt.Errorf("entry #%d: invalid address family %q", index+1, entry.Family)
}
rule := entry.singlePortRule()
if strings.TrimSpace(rule.Protocol) == "" {
rule.Protocol = "tcp"
}
if len(rule.Sources) == 0 {
rule.Sources = []string{"0.0.0.0/0"}
if family == constant.FirewallFamilyIPv6 {
rule.Sources = []string{"::/0"}
} else if family == "" && strings.TrimSpace(rule.Type) != "" {
rule.Sources = append(rule.Sources, "::/0")
}
}
rule.Sources, err = firewall.NormalizeWhitelistSources(family, rule.Sources)
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
normalized, err := service.InitializeFirewallWhitelistPorts([]firewall.PortWhitelist{rule})
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
rule = normalized[0]
if existing, found := indexes[key(rule)]; found {
rules[existing].Sources, err = firewall.NormalizeWhitelistSources("", append(rules[existing].Sources, rule.Sources...))
if err != nil {
return nil, err
}
continue
}
indexes[key(rule)] = len(rules)
rules = append(rules, rule)
}
defaults := []firewall.PortWhitelist{
{Type: firewall.PortWhitelistTypePanel, Protocol: "tcp"},
{Type: firewall.PortWhitelistTypeSSH, Protocol: "tcp"},
{Port: "443", Protocol: "tcp"},
{Port: "443", Protocol: "udp"},
{Port: "80", Protocol: "tcp"},
}
for _, rule := range defaults {
index, found := indexes[key(rule)]
if !found {
index = len(rules)
indexes[key(rule)] = index
rules = append(rules, rule)
}
var ipv4, ipv6 bool
for _, source := range rules[index].Sources {
if strings.Contains(source, ":") {
ipv6 = true
} else {
ipv4 = true
}
}
if !ipv4 {
rules[index].Sources = append(rules[index].Sources, "0.0.0.0/0")
}
if !ipv6 {
rules[index].Sources = append(rules[index].Sources, "::/0")
}
}
return service.InitializeFirewallWhitelistPorts(rules)
}
func parseLegacyPortWhitelist(value string) ([]legacyPortWhitelist, error) {
value = strings.TrimSpace(value)
if value == "" || value == "null" {
return nil, nil
}
if strings.HasPrefix(value, "[") {
var rules []legacyPortWhitelist
err := json.Unmarshal([]byte(value), &rules)
return rules, err
}
items := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ';' || unicode.IsSpace(r) })
rules := make([]legacyPortWhitelist, 0, len(items))
for _, item := range items {
parts := strings.Split(item, "/")
rule := legacyPortWhitelist{}
switch len(parts) {
case 1:
rule.Port = parts[0]
case 2:
rule.Port, rule.Protocol = parts[0], parts[1]
case 3:
rule.Family, rule.Port, rule.Protocol = parts[0], parts[1], parts[2]
default:
return nil, fmt.Errorf("invalid legacy whitelist entry %q", item)
}
rules = append(rules, rule)
}
return rules, nil
}
+7 -132
View File
@@ -19,16 +19,14 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils"
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/go-gormigrate/gormigrate/v2"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -491,7 +489,7 @@ var AddColumnToAlert = &gormigrate.Migration{
var MigrateAlertMethodConfigIDs = &gormigrate.Migration{
ID: "20251001-migrate-alert-method-config-ids",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Alert{}, &model.AlertLog{}, &model.AlertTask{}, &model.AlertConfig{}); err != nil {
if err := global.AlertDB.AutoMigrate(&model.Alert{}, &model.AlertLog{}, &model.AlertTask{}, &model.AlertConfig{}); err != nil {
return err
}
if err := migrateAlertMethodConfigIDs(tx); err != nil {
@@ -504,7 +502,7 @@ var MigrateAlertMethodConfigIDs = &gormigrate.Migration{
var MigrateAlertLogTaskMethodConfigIDs = &gormigrate.Migration{
ID: "20260608-migrate-alert-log-task-method-config-ids",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.AlertLog{}, &model.AlertTask{}, &model.AlertConfig{}); err != nil {
if err := global.AlertDB.AutoMigrate(&model.AlertLog{}, &model.AlertTask{}, &model.AlertConfig{}); err != nil {
return err
}
if err := migrateAlertMethodRecords(tx, &model.AlertLog{}); err != nil {
@@ -520,104 +518,10 @@ var MigrateAlertLogTaskMethodConfigIDs = &gormigrate.Migration{
var AddAlertAuditUser = &gormigrate.Migration{
ID: "20260602-add-alert-audit-user",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.Alert{}, &model.AlertConfig{})
return global.AlertDB.AutoMigrate(&model.Alert{}, &model.AlertConfig{})
},
}
var AddAlertConfigUIDAndSecret = &gormigrate.Migration{
ID: "20260826-add-alert-config-uid-secret",
Migrate: func(tx *gorm.DB) error {
return migrateAlertConfigUIDAndSecret(tx)
},
}
var AddAlertTaskDeliveryLogID = &gormigrate.Migration{
ID: "20260826-add-alert-task-delivery-log-id",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.AlertTask{})
},
}
func migrateAlertConfigUIDAndSecret(tx *gorm.DB) error {
if !tx.Migrator().HasTable(&model.AlertConfig{}) {
return tx.AutoMigrate(&model.AlertConfig{})
}
if !tx.Migrator().HasColumn(&model.AlertConfig{}, "UID") {
if err := tx.Exec("ALTER TABLE alert_configs ADD COLUMN uid varchar(64)").Error; err != nil {
return err
}
}
if !tx.Migrator().HasColumn(&model.AlertConfig{}, "SecretConfig") {
if err := tx.Exec("ALTER TABLE alert_configs ADD COLUMN secret_config text NOT NULL DEFAULT ''").Error; err != nil {
return err
}
}
var configs []struct {
ID uint
UID string
}
if err := tx.Table("alert_configs").Select("id", "uid").Find(&configs).Error; err != nil {
return err
}
for _, config := range configs {
if strings.TrimSpace(config.UID) != "" {
continue
}
if err := tx.Table("alert_configs").Where("id = ?", config.ID).Update("uid", uuid.NewString()).Error; err != nil {
return err
}
}
if err := tx.AutoMigrate(&model.AlertConfig{}); err != nil {
return err
}
var customConfigs []model.AlertConfig
if err := tx.Where("type = ?", constant.Custom).Find(&customConfigs).Error; err != nil {
return err
}
for _, config := range customConfigs {
prepared, status, legacy, err := alertwebhook.NormalizeLegacy(config.Config, config.Status, config.Title)
if err != nil {
return fmt.Errorf("normalize legacy custom webhook %d: %w", config.ID, err)
}
if !legacy {
if err := alertwebhook.ValidateStored(config); err != nil && config.Status != constant.AlertDisable {
if updateErr := tx.Model(&model.AlertConfig{}).Where("id = ?", config.ID).Update("status", constant.AlertDisable).Error; updateErr != nil {
return updateErr
}
}
continue
}
if err := tx.Model(&model.AlertConfig{}).Where("id = ?", config.ID).Updates(map[string]interface{}{
"config": prepared.Config,
"secret_config": prepared.SecretConfig,
"status": status,
}).Error; err != nil {
return err
}
}
if tx.Migrator().HasTable(&model.Alert{}) {
if err := migrateAlertMethodConfigIDs(tx); err != nil {
return err
}
}
if tx.Migrator().HasTable(&model.AlertLog{}) {
if err := migrateAlertMethodRecords(tx, &model.AlertLog{}); err != nil {
return err
}
}
if tx.Migrator().HasTable(&model.AlertTask{}) {
if err := migrateAlertMethodRecords(tx, &model.AlertTask{}); err != nil {
return err
}
}
return nil
}
func migrateAlertMethodConfigIDs(tx *gorm.DB) error {
if err := tx.Model(&model.AlertConfig{}).Where("type = ?", "mail").Update("type", constant.EmailConfig).Error; err != nil {
return err
@@ -711,7 +615,6 @@ func alertLegacyMethodTypeMap() map[string]string {
constant.WeCom: constant.WeCom,
constant.DingTalk: constant.DingTalk,
constant.FeiShu: constant.FeiShu,
constant.Custom: constant.Custom,
}
}
@@ -1131,7 +1034,7 @@ var AddisIPtoWebsiteSSL = &gormigrate.Migration{
var InitPingStatus = &gormigrate.Migration{
ID: "20251201-init-ping-status",
Migrate: func(tx *gorm.DB) error {
status := firewall.LoadPingStatus()
status := ping.LoadStatus()
if err := tx.Create(&model.Setting{Key: "BanPing", Value: status}).Error; err != nil {
return err
}
@@ -1802,22 +1705,12 @@ var NormalizeFirewallBackendSelections = &gormigrate.Migration{
ID: "20260826-normalize-firewall-backend-selections",
Migrate: func(tx *gorm.DB) error {
return tx.Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Setting{}).
Where(
"key = ? AND value NOT IN ?",
constant.FirewallDockerBackendKey,
[]string{constant.FirewallProviderIptables, constant.FirewallProviderNftables},
).
Update("value", constant.FirewallProviderIptables)
if result.Error != nil {
return result.Error
}
if err := tx.Where("key = ?", constant.FirewallDockerBackendKey).FirstOrCreate(&model.Setting{
Key: constant.FirewallDockerBackendKey, Value: constant.FirewallProviderIptables,
Key: constant.FirewallDockerBackendKey, Value: "",
}).Error; err != nil {
return err
}
result = tx.Model(&model.Setting{}).
result := tx.Model(&model.Setting{}).
Where(
"key = ? AND value NOT IN ?",
constant.FirewallForwardingBackendKey,
@@ -1890,21 +1783,3 @@ var SimplifyFirewallRulePolicy = &gormigrate.Migration{
})
},
}
var AddDockerPortGuardReadOnly = &gormigrate.Migration{
ID: "20260902-add-docker-port-guard-read-only",
Migrate: func(tx *gorm.DB) error {
if !tx.Migrator().HasTable(&model.DockerPortGuardPolicy{}) {
return nil
}
if err := tx.AutoMigrate(&model.DockerPortGuardPolicy{}); err != nil {
return err
}
if tx.Migrator().HasIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint") {
if err := tx.Migrator().DropIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint"); err != nil {
return err
}
}
return tx.Migrator().CreateIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint")
},
}
@@ -10,7 +10,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
@@ -25,11 +24,6 @@ type legacyFirewalldForward struct {
spec string
}
type legacyFirewalldForwardFailure struct {
spec string
err error
}
type firewallTransferSource struct {
rules []forwarding.Rule
firewalld []legacyFirewalldForward
@@ -164,14 +158,16 @@ func loadLegacyFirewallForwarding() (firewallTransferSource, error) {
if err != nil {
return firewallTransferSource{}, err
}
supportedRules, cleanupRules, failures := selectSupportedLegacyFirewalldForwarding(firewalldRules)
for _, failure := range failures {
if global.LOG != nil {
global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err)
for _, item := range firewalldRules {
if _, err := forwarding.NormalizeRule(item.rule); err != nil {
if global.LOG != nil {
global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", item.spec, err)
}
continue
}
source.rules = append(source.rules, item.rule)
source.firewalld = append(source.firewalld, item)
}
source.rules = append(source.rules, supportedRules...)
source.firewalld = append(source.firewalld, cleanupRules...)
if len(source.rules) > 0 {
source.provider = "iptables"
}
@@ -188,23 +184,6 @@ func loadLegacyFirewallForwarding() (firewallTransferSource, error) {
return source, nil
}
func selectSupportedLegacyFirewalldForwarding(items []legacyFirewalldForward) (
[]forwarding.Rule, []legacyFirewalldForward, []legacyFirewalldForwardFailure,
) {
rules := make([]forwarding.Rule, 0, len(items))
cleanup := make([]legacyFirewalldForward, 0, len(items))
failures := make([]legacyFirewalldForwardFailure, 0)
for _, item := range items {
if _, err := forwarding.NormalizeRule(item.rule); err != nil {
failures = append(failures, legacyFirewalldForwardFailure{spec: item.spec, err: err})
continue
}
rules = append(rules, item.rule)
cleanup = append(cleanup, item)
}
return rules, cleanup, failures
}
func listLegacyIptablesForwarding() ([]forwarding.Rule, error) {
exists, err := iptables_helper.CheckChainExist(iptables_helper.NatTab, forwarding.ChainPreRouting)
if err != nil {
@@ -281,55 +260,40 @@ func listLegacyFirewalldForwarding() ([]legacyFirewalldForward, error) {
if err != nil {
return nil, fmt.Errorf("list legacy firewalld forwarding rules: %w", err)
}
rules, failures := parseLegacyFirewalldForwarding(stdout)
for _, failure := range failures {
if global.LOG != nil {
global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err)
}
}
return rules, nil
return parseLegacyFirewalldForwarding(stdout), nil
}
func parseLegacyFirewalldForwarding(stdout string) ([]legacyFirewalldForward, []legacyFirewalldForwardFailure) {
func parseLegacyFirewalldForwarding(stdout string) []legacyFirewalldForward {
result := make([]legacyFirewalldForward, 0)
failures := make([]legacyFirewalldForwardFailure, 0)
for _, spec := range strings.Fields(stdout) {
item, err := parseLegacyFirewalldForward(spec)
if err != nil {
failures = append(failures, legacyFirewalldForwardFailure{spec: spec, err: err})
for _, line := range strings.Split(stdout, "\n") {
spec := strings.TrimSpace(line)
if !strings.HasPrefix(spec, "port=") {
continue
}
result = append(result, item)
port, rest, ok := strings.Cut(strings.TrimPrefix(spec, "port="), ":proto=")
if !ok {
continue
}
protocol, rest, ok := strings.Cut(rest, ":toport=")
if !ok {
continue
}
targetPort, targetIP, ok := strings.Cut(rest, ":toaddr=")
if !ok {
continue
}
if targetIP == "" {
targetIP = "127.0.0.1"
}
result = append(result, legacyFirewalldForward{
rule: forwarding.Rule{
Family: forwarding.FamilyIPv4, Protocol: protocol, Port: port,
TargetIP: targetIP, TargetPort: targetPort,
},
spec: spec,
})
}
return result, failures
}
func parseLegacyFirewalldForward(spec string) (legacyFirewalldForward, error) {
if !strings.HasPrefix(spec, "port=") {
return legacyFirewalldForward{}, errors.New("missing port field")
}
port, rest, ok := strings.Cut(strings.TrimPrefix(spec, "port="), ":proto=")
if !ok {
return legacyFirewalldForward{}, errors.New("missing protocol field")
}
protocol, rest, ok := strings.Cut(rest, ":toport=")
if !ok {
return legacyFirewalldForward{}, errors.New("missing target port field")
}
targetPort, targetIP, ok := strings.Cut(rest, ":toaddr=")
if !ok {
return legacyFirewalldForward{}, errors.New("missing target address field")
}
if targetIP == "" {
targetIP = "127.0.0.1"
}
return legacyFirewalldForward{
rule: forwarding.Rule{
Family: forwarding.FamilyIPv4, Protocol: protocol, Port: port,
TargetIP: targetIP, TargetPort: targetPort,
},
spec: spec,
}, nil
return result
}
func cleanupLegacyFirewalldForwarding(rules []legacyFirewalldForward) error {
@@ -347,27 +311,5 @@ func cleanupLegacyFirewalldForwarding(rules []legacyFirewalldForward) error {
if err := manager.Run("firewall-cmd", "--reload"); err != nil {
return fmt.Errorf("reload firewalld after forwarding transfer: %w", err)
}
return restartDockerAfterFirewalldReload(cmd.Which, controller.CheckActive, controller.HandleRestart)
}
func restartDockerAfterFirewalldReload(
which func(string) bool,
checkActive func(string) (bool, error),
restart func(string) error,
) error {
const service = "docker"
if !which(service) {
return nil
}
active, err := checkActive(service)
if err != nil {
return fmt.Errorf("check Docker status after reloading firewalld: %w", err)
}
if !active {
return nil
}
if err := restart(service); err != nil {
return fmt.Errorf("restart Docker after reloading firewalld: %w", err)
}
return nil
}
@@ -34,6 +34,9 @@ type legacyHostFirewallRecord struct {
Description string
}
// TransferHostFirewall imports the legacy firewalls table into firewall_rules.
// It intentionally does not inspect or mutate the system firewall: the normal
// inventory merge associates imported rows with observed rules by RuleKey.
func TransferHostFirewall(ctx context.Context, provider string) error {
if global.DB == nil {
return errors.New("host firewall transfer database is required")
@@ -41,47 +44,6 @@ func TransferHostFirewall(ctx context.Context, provider string) error {
return transferHostFirewall(ctx, global.DB, filter.Provider(strings.ToLower(strings.TrimSpace(provider))))
}
func TransferLegacyHostFirewallRuleOwnership(ctx context.Context, provider string, transfer func(context.Context) error) error {
if global.DB == nil {
return errors.New("host firewall transfer database is required")
}
return transferLegacyHostFirewallRuleOwnership(
ctx,
global.DB,
filter.Provider(strings.ToLower(strings.TrimSpace(provider))),
transfer,
)
}
func transferLegacyHostFirewallRuleOwnership(
ctx context.Context,
db *gorm.DB,
provider filter.Provider,
transfer func(context.Context) error,
) error {
if !legacyHostFirewallOwnershipProvider(provider) {
return nil
}
if db == nil {
return errors.New("host firewall transfer database is required")
}
completed, err := migrationRecordExists(db, hostFirewallTransferMigrationID)
if err != nil || completed {
return err
}
if transfer == nil {
return errors.New("legacy host firewall ownership transfer is required")
}
if err := transfer(ctx); err != nil {
return fmt.Errorf("transfer legacy host firewall rule ownership: %w", err)
}
return markMigrationRecord(db, hostFirewallTransferMigrationID)
}
func legacyHostFirewallOwnershipProvider(provider filter.Provider) bool {
return provider == filter.ProviderIptables || provider == filter.ProviderUFW
}
func transferHostFirewall(ctx context.Context, db *gorm.DB, provider filter.Provider) error {
if db == nil {
return errors.New("host firewall transfer database is required")
@@ -107,9 +69,6 @@ func transferHostFirewall(ctx context.Context, db *gorm.DB, provider filter.Prov
if err := importLegacyHostFirewallRules(tx, models); err != nil {
return err
}
if legacyHostFirewallOwnershipProvider(provider) {
return nil
}
return markMigrationRecord(tx, hostFirewallTransferMigrationID)
})
}
@@ -185,7 +144,7 @@ func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Pr
rule.SourcePort = ""
rule.DestinationPort = ""
default:
if provider != filter.ProviderIptables || legacyIptablesAdvancedChain(record.Chain) {
if provider != filter.ProviderIptables {
return nil, fmt.Errorf("%w: advanced rule for provider %q", errUnsupportedLegacyHostFirewallRule, provider)
}
}
@@ -207,15 +166,6 @@ func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Pr
return filter.ExpandAtomicRules(rule)
}
func legacyIptablesAdvancedChain(chain string) bool {
switch strings.ToUpper(strings.TrimSpace(chain)) {
case "1PANEL_INPUT", "1PANEL_OUTPUT":
return true
default:
return false
}
}
func legacyIptablesChain(record legacyHostFirewallRecord) string {
typeName := strings.ToLower(strings.TrimSpace(record.Type))
if typeName == "port" || typeName == "address" || typeName == "ip" {
@@ -251,9 +201,6 @@ func legacyUFWHostRules(record legacyHostFirewallRecord, rule filter.FirewallRul
if strings.EqualFold(strings.TrimSpace(record.Type), "address") || strings.EqualFold(strings.TrimSpace(record.Type), "ip") {
rule.SourceAddress, rule.DestinationAddress = splitLegacyUFWAddress(rule.SourceAddress)
}
if legacyUFWSinglePortAllProtocols(record, rule.DestinationPort) {
rule.Protocol = "all"
}
if legacyAddressIsEmpty(rule.SourceAddress) && legacyAddressIsEmpty(rule.DestinationAddress) {
rule.Scope.Family = filter.FamilyInet
} else {
@@ -262,18 +209,6 @@ func legacyUFWHostRules(record legacyHostFirewallRecord, rule filter.FirewallRul
return filter.ExpandAtomicRules(rule)
}
func legacyUFWSinglePortAllProtocols(record legacyHostFirewallRecord, port string) bool {
if !strings.EqualFold(strings.TrimSpace(record.Type), "port") {
return false
}
protocol := strings.ToLower(strings.TrimSpace(record.Protocol))
if protocol != "tcp/udp" && protocol != "udp/tcp" {
return false
}
port = strings.TrimSpace(port)
return port != "" && !strings.Contains(port, ",") && !strings.Contains(port, "-")
}
func expandLegacyFamilies(rule filter.FirewallRule, families ...filter.Family) ([]filter.FirewallRule, error) {
result := make([]filter.FirewallRule, 0, len(families))
for _, family := range families {
-7
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"path"
"strconv"
"github.com/1Panel-dev/1Panel/agent/cmd/server/conf"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -62,10 +61,4 @@ func initBaseInfo() {
panic(err)
}
global.CONF.Base.InstallDir = nodeInfo.BaseDir
if !global.IsMaster {
global.CONF.Base.Port = strconv.FormatUint(uint64(nodeInfo.NodePort), 10)
if nodeInfo.NodePort == 0 {
global.CONF.Base.Port = "9999"
}
}
}
-1
View File
@@ -24,7 +24,6 @@ func (a *AlertRouter) InitRouter(Router *gin.RouterGroup) {
alertRouter.POST("/cronjob/list", baseApi.GetCronJobs)
alertRouter.POST("/config/update", baseApi.UpdateAlertConfig)
alertRouter.POST("/config/status", baseApi.UpdateAlertConfigStatus)
alertRouter.POST("/config/info", baseApi.GetAlertConfig)
alertRouter.POST("/config/search", baseApi.PageAlertConfig)
alertRouter.POST("/config/del", baseApi.DeleteAlertConfig)
-1
View File
@@ -43,7 +43,6 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
fileRouter.POST("/rename", baseApi.ChangeFileName)
fileRouter.POST("/wget", baseApi.WgetFile)
fileRouter.POST("/wget/stop", baseApi.StopWget)
fileRouter.POST("/wget/process/remove", baseApi.RemoveWgetRecords)
fileRouter.POST("/move", baseApi.MoveFile)
fileRouter.POST("/move/stop", baseApi.StopMoveFile)
fileRouter.GET("/download", baseApi.Download)
+1 -10
View File
@@ -10,7 +10,6 @@ type HostRouter struct{}
func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter := Router.Group("hosts")
baseApi := v2.ApiGroupApp.BaseApi
Router.POST("/internal/terminal/sessions/revoke", baseApi.RevokeTerminalSessions)
{
hostRouter.POST("", baseApi.CreateHost)
hostRouter.POST("/info", baseApi.GetHostByID)
@@ -24,12 +23,8 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/base", baseApi.LoadFirewallBaseInfo)
hostRouter.POST("/firewall/operate", baseApi.OperateFirewall)
hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort)
hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings)
hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend)
hostRouter.POST("/firewall/settings/whitelist", baseApi.CreateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/update", baseApi.UpdateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/delete", baseApi.DeleteFirewallPortWhitelist)
hostRouter.POST("/firewall/forward/base", baseApi.LoadForwardingBaseInfo)
hostRouter.POST("/firewall/forward/search", baseApi.SearchForwardingRules)
hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules)
@@ -37,7 +32,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/rules/search", baseApi.SearchFirewallRules)
hostRouter.POST("/firewall/rules/reset", baseApi.ResetFirewallRules)
hostRouter.POST("/firewall/rules/native/detail", baseApi.LoadFirewallNativeDetail)
hostRouter.POST("/firewall/rules/adopt", baseApi.AdoptFirewallRule)
hostRouter.POST("/firewall/rules/check", baseApi.CheckFirewallRules)
hostRouter.POST("/firewall/rules", baseApi.CreateFirewallRules)
hostRouter.POST("/firewall/rules/sync/preview", baseApi.PreviewFirewallRuleSync)
hostRouter.GET("/firewall/rules/sync/task", baseApi.LoadFirewallRuleSyncTask)
@@ -48,7 +43,6 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/filter/operate", baseApi.OperateFilterChain)
hostRouter.GET("/firewall/docker/ports", baseApi.ListDockerPortGuard)
hostRouter.GET("/firewall/docker/endpoints", baseApi.ListDockerPublishedPorts)
hostRouter.POST("/firewall/docker/sync", baseApi.SyncDockerPortGuard)
hostRouter.POST("/firewall/docker/operate", baseApi.OperateDockerPortGuard)
hostRouter.POST("/firewall/docker/policies/batch", baseApi.UpsertDockerPortGuardPolicies)
@@ -89,9 +83,6 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.GET("/terminal/local", baseApi.WsLocalTerminal)
hostRouter.GET("/terminal/ssh", baseApi.WsHostSSH)
hostRouter.GET("/terminal/container", baseApi.WsContainerTerminal)
hostRouter.POST("/terminal/sessions/search", baseApi.SearchTerminalSessions)
hostRouter.POST("/terminal/sessions/close", baseApi.CloseTerminalSession)
hostRouter.POST("/terminal/sessions/closeAll", baseApi.CloseAllTerminalSessions)
hostRouter.GET("/disks", baseApi.GetCompleteDiskInfo)
hostRouter.POST("/disks/partition", baseApi.PartitionDisk)
-4
View File
@@ -29,7 +29,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/init/validator"
"github.com/1Panel-dev/1Panel/agent/init/viper"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
@@ -97,9 +96,6 @@ func Start() {
dir.Init()
log.Init()
global.LOG.Info("agent startup: logger initialized")
if err := files.CleanupInterruptedDownloads(); err != nil {
global.LOG.Warnf("clean interrupted remote downloads: %s", err)
}
db.Init()
global.LOG.Info("agent startup: database initialized")
migration.Init()
+131 -111
View File
@@ -2,10 +2,13 @@ package alert
import (
"encoding/json"
"errors"
"fmt"
"mime"
network "net"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"sync"
@@ -21,6 +24,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/bark"
"github.com/1Panel-dev/1Panel/agent/utils/email"
"github.com/1Panel-dev/1Panel/agent/utils/psutil"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/jinzhu/copier"
)
@@ -153,117 +157,6 @@ func CreateNewAlertTask(quota, alertType, quotaType, method string) {
global.LOG.Infof("%s alert %s push completed", alertType, method)
}
func AttachQueuedAlertTaskMetadata(rawDetail string, task dto.AlertTaskMetadata) (string, error) {
if err := validateQueuedAlertTaskMetadata(task); err != nil {
return "", err
}
var detail dto.AlertDetail
if err := json.Unmarshal([]byte(rawDetail), &detail); err != nil {
return "", fmt.Errorf("decode queued alert detail: %w", err)
}
detail.Task = &task
data, err := json.Marshal(detail)
if err != nil {
return "", fmt.Errorf("encode queued alert detail: %w", err)
}
return string(data), nil
}
func RecordQueuedAlertTask(logID uint, task dto.AlertTaskMetadata) (bool, error) {
if logID == 0 {
return false, fmt.Errorf("queued alert log ID is required")
}
if global.AlertDB == nil {
return false, fmt.Errorf("alert database is unavailable")
}
if err := validateQueuedAlertTaskMetadata(task); err != nil {
return false, err
}
alertRepo := repo.NewIAlertRepo()
log, err := alertRepo.GetLog(repo.WithByID(logID))
if err != nil {
return false, err
}
if log.Status != constant.AlertPushing {
if log.Status == constant.AlertSuccess {
if existing, taskErr := alertRepo.GetAlertTask(alertRepo.WithByDeliveryLogID(logID)); taskErr == nil && existing.DeliveryLogID != nil {
return false, nil
}
}
return false, fmt.Errorf("alert delivery log %d is not pending", logID)
}
storedTask, alertTask, err := queuedAlertTaskFromLog(log)
if err != nil {
return false, err
}
if storedTask != task {
return false, fmt.Errorf("queued alert task metadata changed before reservation")
}
return alertRepo.CreatePendingAlertTask(logID, task.AlertID, &alertTask)
}
func FinalizeQueuedAlertDelivery(logID uint, succeeded bool, message string) (bool, error) {
if logID == 0 {
return false, fmt.Errorf("queued alert log ID is required")
}
if global.AlertDB == nil {
return false, fmt.Errorf("alert database is unavailable")
}
var fallback *model.AlertTask
if succeeded {
alertRepo := repo.NewIAlertRepo()
log, err := alertRepo.GetLog(repo.WithByID(logID))
if err != nil {
return false, err
}
if log.Status != constant.AlertPushing {
return false, nil
}
_, task, err := queuedAlertTaskFromLog(log)
if err != nil {
return false, err
}
fallback = &task
}
return repo.NewIAlertRepo().FinalizePendingAlertTask(logID, succeeded, message, fallback)
}
func queuedAlertTaskFromLog(log model.AlertLog) (dto.AlertTaskMetadata, model.AlertTask, error) {
var detail dto.AlertDetail
if err := json.Unmarshal([]byte(log.AlertDetail), &detail); err != nil {
return dto.AlertTaskMetadata{}, model.AlertTask{}, fmt.Errorf("decode queued alert task metadata: %w", err)
}
if detail.Task == nil {
return dto.AlertTaskMetadata{}, model.AlertTask{}, fmt.Errorf("queued alert task metadata is missing")
}
if err := validateQueuedAlertTaskMetadata(*detail.Task); err != nil {
return dto.AlertTaskMetadata{}, model.AlertTask{}, err
}
if detail.Task.AlertID != log.AlertId || detail.Task.Type != log.Type || detail.Task.Method != log.Method {
return dto.AlertTaskMetadata{}, model.AlertTask{}, fmt.Errorf("queued alert task metadata does not match its log")
}
task := model.AlertTask{
Type: detail.Task.Type,
Quota: detail.Task.Quota,
QuotaType: detail.Task.QuotaType,
Method: detail.Task.Method,
}
return *detail.Task, task, nil
}
func validateQueuedAlertTaskMetadata(task dto.AlertTaskMetadata) error {
if task.AlertID == 0 {
return fmt.Errorf("queued alert task alert ID is required")
}
if strings.TrimSpace(task.Type) == "" {
return fmt.Errorf("queued alert task type is required")
}
if strings.TrimSpace(task.Method) == "" {
return fmt.Errorf("queued alert task method is required")
}
return nil
}
func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param, method string) string {
alertDetail := dto.AlertDetail{
Type: GetCronJobType(alert.Type),
@@ -532,6 +425,133 @@ func FindRecentSuccessLoginsNotInWhitelist(minutes int, whitelist []string) ([]m
return abnormalLogs, nil
}
func CountRecentFailedSSHLog(minutes uint, maxAllowed uint) (int, bool, error) {
lines, err := grepSSHLog([]string{"Failed password", "Invalid user", "authentication failure"})
if err != nil {
return 0, false, err
}
thresholdTime := time.Now().Add(-time.Duration(minutes) * time.Minute)
count := 0
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
t, err := parseLogTime(line)
if err != nil {
continue
}
if t.After(thresholdTime) {
count++
}
}
return count, count >= int(maxAllowed), nil
}
func FindRecentSuccessLoginNotInWhitelist(minutes int, whitelist []string) ([]string, error) {
lines, err := grepSSHLog([]string{"Accepted password", "Accepted publickey"})
if err != nil {
return nil, err
}
thresholdTime := time.Now().Add(-time.Duration(minutes) * time.Minute)
var abnormalLogins []string
whitelistMap := make(map[string]struct{}, len(whitelist))
for _, ip := range whitelist {
whitelistMap[ip] = struct{}{}
}
ipRegex := re.GetRegex(re.AlertIPPattern)
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
t, err := parseLogTime(line)
if err != nil || t.Before(thresholdTime) {
continue
}
match := ipRegex.FindStringSubmatch(line)
if len(match) >= 2 {
ip := match[1]
if _, ok := whitelistMap[ip]; !ok {
abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", ip, t.Format("2006-01-02 15:04:05")))
}
}
}
return abnormalLogins, nil
}
func findGrepPath() (string, error) {
path, err := exec.LookPath("grep")
if err != nil {
return "", fmt.Errorf("grep not found in PATH: %w", err)
}
return path, nil
}
func grepSSHLog(keywords []string) ([]string, error) {
logFiles := []string{"/var/log/secure", "/var/log/auth.log"}
var results []string
seen := make(map[string]struct{})
grepPath, err := findGrepPath()
if err != nil {
return nil, fmt.Errorf("find grep failed: %w", err)
}
for _, logFile := range logFiles {
if _, err := os.Stat(logFile); err != nil {
continue
}
for _, keyword := range keywords {
cmd := exec.Command(grepPath, "-a", keyword, logFile)
output, err := cmd.Output()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if exitErr.ExitCode() == 1 {
continue
}
}
return nil, fmt.Errorf("read log file fail [%s]: %w", logFile, err)
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
if _, exists := seen[line]; !exists {
results = append(results, line)
seen[line] = struct{}{}
}
}
}
}
}
return results, nil
}
func parseLogTime(line string) (time.Time, error) {
if len(line) < 15 {
return time.Time{}, nil
}
timeStr := line[:15]
parsedTime, err := time.ParseInLocation("Jan 2 15:04:05", timeStr, time.Local)
if err != nil {
return time.Time{}, nil
}
return parsedTime.AddDate(time.Now().Year(), 0, 0), nil
}
func getNodeName(agentInfo *dto.AgentInfo) string {
var nodeName string
if agentInfo != nil && agentInfo.NodeName != "" {
-206
View File
@@ -1,206 +0,0 @@
package alert
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
"github.com/1Panel-dev/1Panel/agent/utils/webhook_sender"
)
var customWebhookNow = time.Now
func CreateCustomWebhookAlertLog(
alertType string,
info dto.AlertDTO,
create dto.AlertLogCreate,
project string,
params []dto.Param,
config model.AlertConfig,
transport *http.Transport,
agentInfo *dto.AgentInfo,
) error {
alertInfo := info
alertInfo.Type = alertType
create.Type = GetCronJobType(alertType)
create.AlertRule = ProcessAlertRule(info)
create.AlertDetail = ProcessAlertDetail(alertInfo, project, params, constant.Custom)
return deliverCustomWebhook(create, config, transport, agentInfo, customWebhookNow())
}
func CreateTaskScanCustomWebhookAlertLog(
info dto.AlertDTO,
alertType string,
create dto.AlertLogCreate,
pushAlert dto.PushAlert,
config model.AlertConfig,
transport *http.Transport,
agentInfo *dto.AgentInfo,
) error {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
alertInfo := info
alertInfo.Type = alertType
create.Type = GetCronJobType(alertType)
create.AlertRule = ProcessAlertRule(info)
create.AlertDetail = ProcessAlertDetail(alertInfo, pushAlert.TaskName, params, constant.Custom)
return deliverCustomWebhook(create, config, transport, agentInfo, customWebhookNow())
}
func deliverCustomWebhook(
create dto.AlertLogCreate,
config model.AlertConfig,
transport *http.Transport,
agentInfo *dto.AgentInfo,
occurredAt time.Time,
) error {
var alertLog model.AlertLog
templateData, err := customWebhookTemplateData(create.AlertDetail, agentInfo, occurredAt)
if err != nil {
return saveCustomWebhookDeliveryError(create, &alertLog, err)
}
request, err := buildCustomWebhookRequest(config, templateData, transport)
if err != nil {
return saveCustomWebhookDeliveryError(create, &alertLog, err)
}
if _, err := webhook_sender.Execute(context.Background(), request); err != nil {
return saveCustomWebhookDeliveryError(create, &alertLog, err)
}
create.Status = constant.AlertSuccess
return SaveAlertLog(create, &alertLog)
}
func saveCustomWebhookDeliveryError(create dto.AlertLogCreate, alertLog *model.AlertLog, deliveryErr error) error {
create.Status = constant.AlertError
create.Message = deliveryErr.Error()
if err := SaveAlertLog(create, alertLog); err != nil {
global.LOG.Errorf("save custom webhook delivery error log failed: %v", err)
}
return deliveryErr
}
func customWebhookTemplateData(rawDetail string, agentInfo *dto.AgentInfo, occurredAt time.Time) (webhook_sender.TemplateData, error) {
var detail dto.AlertDetail
if err := json.Unmarshal([]byte(rawDetail), &detail); err != nil {
return webhook_sender.TemplateData{}, errors.New("resolve custom webhook alert detail failed")
}
businessType := detail.SubType
if businessType == "" {
businessType = detail.Type
}
if businessType == "" {
return webhook_sender.TemplateData{}, errors.New("resolve custom webhook alert detail failed")
}
content := GetSendContent(businessType, detail.Params, agentInfo)
if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": detail.Title})
}
message := strings.TrimSpace(webhook_sender.NormalizeToText(content))
if message == "" {
message = detail.Title
}
return webhook_sender.TemplateData{
Title: detail.Title,
Message: message,
Type: businessType,
NodeName: customWebhookNodeName(agentInfo),
Timestamp: occurredAt,
}, nil
}
func customWebhookNodeName(agentInfo *dto.AgentInfo) string {
if agentInfo != nil && strings.TrimSpace(agentInfo.NodeName) != "" {
return strings.TrimSpace(agentInfo.NodeName)
}
return strings.TrimSpace(getFallbackHostname())
}
func buildCustomWebhookRequest(
config model.AlertConfig,
data webhook_sender.TemplateData,
transport *http.Transport,
) (webhook_sender.Request, error) {
resolved, err := alert_webhook.Resolve(config)
if err != nil {
return webhook_sender.Request{}, errors.New("resolve custom webhook config failed")
}
return buildResolvedCustomWebhookRequest(resolved, data, transport)
}
func TestCustomWebhook(
resolved dto.AlertCustomWebhookResolvedConfig,
transport *http.Transport,
agentInfo *dto.AgentInfo,
) (dto.AlertConfigTestResult, error) {
request, err := buildResolvedCustomWebhookRequest(resolved, webhook_sender.TemplateData{
Title: "1Panel Webhook Test",
Message: "This is a test notification from 1Panel.",
Type: "test",
NodeName: customWebhookNodeName(agentInfo),
Timestamp: customWebhookNow(),
}, transport)
if err != nil {
return dto.AlertConfigTestResult{}, err
}
request.CaptureResponse = true
result, executeErr := webhook_sender.Execute(context.Background(), request)
durationMillis := result.Duration.Milliseconds()
if result.Duration > 0 && durationMillis == 0 {
durationMillis = 1
}
testResult := dto.AlertConfigTestResult{
Success: executeErr == nil,
StatusCode: result.StatusCode,
Duration: durationMillis,
Response: result.Response,
}
if executeErr != nil {
testResult.Message = executeErr.Error()
}
return testResult, nil
}
func buildResolvedCustomWebhookRequest(
resolved dto.AlertCustomWebhookResolvedConfig,
data webhook_sender.TemplateData,
transport *http.Transport,
) (webhook_sender.Request, error) {
preset, err := webhook_sender.ResolvePreset(resolved.Preset)
if err != nil {
return webhook_sender.Request{}, errors.New("render custom webhook request failed")
}
format := webhook_sender.BodyFormat(resolved.Body.Type)
fields := make([]webhook_sender.FormField, 0, len(resolved.Body.Fields))
for _, field := range resolved.Body.Fields {
fields = append(fields, webhook_sender.FormField{Key: field.Key, Value: field.Value})
}
body, err := webhook_sender.RenderBody(webhook_sender.RenderRequest{
Format: format,
Template: resolved.Body.Template,
Fields: fields,
Data: data,
})
if err != nil {
return webhook_sender.Request{}, errors.New("render custom webhook request failed")
}
headers := make(map[string]string, len(resolved.Headers))
for _, header := range resolved.Headers {
headers[header.Key] = header.Value
}
return webhook_sender.Request{
URL: resolved.URL,
Preset: preset,
Format: format,
Body: body,
Headers: headers,
Transport: transport,
}, nil
}
-312
View File
@@ -1,312 +0,0 @@
package alert_config
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
"unicode/utf8"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
)
const MaskedSecret = "******"
type secretMutation struct {
Action string `json:"action"`
Value string `json:"value,omitempty"`
}
func IsLegacySecretType(configType string) bool {
return secretField(configType) != ""
}
func UsesMutation(configType, rawMutation string) (bool, error) {
field := secretField(configType)
if field == "" {
return false, nil
}
root, err := decodeObject(rawMutation)
if err != nil {
return false, fmt.Errorf("decode alert config mutation: %w", err)
}
if raw, ok := root[field]; ok && rawIsObject(raw) {
return true, nil
}
if !isWebhookType(configType) {
return false, nil
}
raw, ok := root["webhooks"]
if !ok {
return false, nil
}
var items []map[string]json.RawMessage
if err := json.Unmarshal(raw, &items); err != nil {
return false, fmt.Errorf("webhooks mutation must be an array")
}
for _, item := range items {
if rawURL, ok := item["url"]; ok && rawIsObject(rawURL) {
return true, nil
}
}
return false, nil
}
func Prepare(configType, rawMutation, status string, existing *model.AlertConfig) (string, error) {
field := secretField(configType)
if field == "" {
return rawMutation, nil
}
root, err := decodeObject(rawMutation)
if err != nil {
return "", fmt.Errorf("decode alert config mutation: %w", err)
}
var existingRoot map[string]json.RawMessage
if existing != nil {
if existing.Type != configType {
return "", fmt.Errorf("alert config %d has type %s, not %s", existing.ID, existing.Type, configType)
}
existingRoot, err = decodeObject(existing.Config)
if err != nil {
return "", fmt.Errorf("decode stored alert config: %w", err)
}
}
existingSecret, err := storedSecret(existingRoot, field)
if err != nil {
return "", err
}
if raw, ok := root[field]; ok {
value, err := mergeSecret(field, raw, existingSecret, existing != nil)
if err != nil {
return "", fmt.Errorf("merge alert config %s: %w", field, err)
}
root[field] = mustJSON(value)
} else if existing != nil {
root[field] = mustJSON(existingSecret)
}
if isWebhookType(configType) {
if err := mergeWebhookArray(root, existingRoot, existing != nil); err != nil {
return "", err
}
}
if status == constant.AlertEnable {
if configType == constant.SMSConfig {
phone, err := storedSecret(root, "phone")
if err != nil || phone == "" {
return "", fmt.Errorf("SMS phone is required while the config is enabled")
}
}
if isWebhookType(configType) && !hasWebhookURL(root) {
return "", fmt.Errorf("webhook URL is required while the config is enabled")
}
}
return encodeObject(root)
}
func secretField(configType string) string {
switch configType {
case constant.EmailConfig:
return "password"
case constant.SMSConfig:
return "phone"
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark:
return "url"
default:
return ""
}
}
func isWebhookType(configType string) bool {
switch configType {
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark:
return true
default:
return false
}
}
func decodeObject(raw string) (map[string]json.RawMessage, error) {
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.DisallowUnknownFields()
var result map[string]json.RawMessage
if err := decoder.Decode(&result); err != nil {
return nil, err
}
if result == nil {
return nil, fmt.Errorf("alert config must be a JSON object")
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return nil, fmt.Errorf("multiple JSON values are not allowed")
}
return nil, err
}
return result, nil
}
func encodeObject(value map[string]json.RawMessage) (string, error) {
encoded, err := json.Marshal(value)
if err != nil {
return "", fmt.Errorf("encode alert config: %w", err)
}
return string(encoded), nil
}
func mustJSON(value any) json.RawMessage {
encoded, err := json.Marshal(value)
if err != nil {
panic(err)
}
return encoded
}
func decodeStoredSecret(raw json.RawMessage) (string, error) {
var value string
if err := json.Unmarshal(raw, &value); err != nil {
return "", fmt.Errorf("stored secret is not a string")
}
return value, nil
}
func storedSecret(root map[string]json.RawMessage, field string) (string, error) {
if root == nil {
return "", nil
}
raw, ok := root[field]
if !ok {
return "", nil
}
value, err := decodeStoredSecret(raw)
if err != nil {
return "", fmt.Errorf("stored alert config %s is invalid", field)
}
return value, nil
}
func mergeSecret(field string, raw json.RawMessage, existing string, hasExisting bool) (string, error) {
if value, err := decodeStoredSecret(raw); err == nil {
if hasExisting && isLegacyMaskValue(field, value) {
return existing, nil
}
return value, nil
}
var mutation secretMutation
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&mutation); err != nil {
return "", fmt.Errorf("secret mutation must be a string or keep/replace/clear object")
}
switch mutation.Action {
case "keep":
if !hasExisting {
return "", fmt.Errorf("secret cannot be kept because the config does not exist")
}
return existing, nil
case "replace":
return mutation.Value, nil
case "clear":
return "", nil
default:
return "", fmt.Errorf("secret action must be keep, replace, or clear")
}
}
func isLegacyMaskValue(field, value string) bool {
if field == "phone" {
return isLegacyMaskedPhone(value)
}
return value == MaskedSecret
}
func isLegacyMaskedPhone(value string) bool {
value = strings.TrimSpace(value)
if !strings.Contains(value, "*") {
return false
}
return maskPhone(strings.ReplaceAll(value, "*", "0")) == value
}
func rawIsObject(raw json.RawMessage) bool {
trimmed := bytes.TrimSpace(raw)
return len(trimmed) > 0 && trimmed[0] == '{'
}
func mergeWebhookArray(root, existingRoot map[string]json.RawMessage, hasExisting bool) error {
raw, ok := root["webhooks"]
if !ok {
return nil
}
var items []map[string]json.RawMessage
if err := json.Unmarshal(raw, &items); err != nil {
return fmt.Errorf("webhooks mutation must be an array")
}
var existingItems []map[string]json.RawMessage
if existingRoot != nil {
if existingRaw, ok := existingRoot["webhooks"]; ok {
if err := json.Unmarshal(existingRaw, &existingItems); err != nil {
return fmt.Errorf("stored webhooks must be an array")
}
}
}
for index := range items {
rawURL, ok := items[index]["url"]
if !ok {
continue
}
var existingURL string
itemExists := hasExisting && index < len(existingItems)
if itemExists {
var err error
existingURL, err = storedSecret(existingItems[index], "url")
if err != nil {
return fmt.Errorf("stored webhook URL %d is invalid", index)
}
}
value, err := mergeSecret("url", rawURL, existingURL, itemExists)
if err != nil {
return fmt.Errorf("merge webhook URL %d: %w", index, err)
}
items[index]["url"] = mustJSON(value)
}
root["webhooks"] = mustJSON(items)
return nil
}
func hasWebhookURL(root map[string]json.RawMessage) bool {
if value, err := storedSecret(root, "url"); err == nil && value != "" {
return true
}
raw, ok := root["webhooks"]
if !ok {
return false
}
var items []map[string]json.RawMessage
if err := json.Unmarshal(raw, &items); err != nil {
return false
}
for _, item := range items {
if value, err := storedSecret(item, "url"); err == nil && value != "" {
return true
}
}
return false
}
func maskPhone(value string) string {
runes := []rune(strings.TrimSpace(value))
switch {
case len(runes) >= 11:
return string(runes[:3]) + strings.Repeat("*", len(runes)-7) + string(runes[len(runes)-4:])
case len(runes) >= 8:
return string(runes[:2]) + strings.Repeat("*", len(runes)-4) + string(runes[len(runes)-2:])
case utf8.RuneCountInString(value) == 0:
return ""
default:
return MaskedSecret
}
}
+6 -25
View File
@@ -50,10 +50,9 @@ func pushByConfigId(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto
func pushByLegacyMethod(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.PushAlert, method string) {
typeMap := map[string]string{
"mail": constant.Email,
constant.Bark: constant.Bark,
constant.SMS: constant.SMS,
constant.Custom: constant.Custom,
"mail": constant.Email,
constant.Bark: constant.Bark,
constant.SMS: constant.SMS,
}
configType := method
if mapped, ok := typeMap[method]; ok {
@@ -138,7 +137,7 @@ func sendAlert(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.Push
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
case constant.WeCom, constant.DingTalk, constant.FeiShu:
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
if err != nil || alert.SendCount <= todayCount {
return
@@ -151,29 +150,11 @@ func sendAlert(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.Push
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false
if config.Type == constant.Custom {
task := dto.AlertTaskMetadata{
AlertID: alert.ID,
Type: alertUtil.GetCronJobType(alert.Type),
Quota: strconv.Itoa(int(pushAlert.EntryID)),
QuotaType: strconv.Itoa(int(pushAlert.EntryID)),
Method: methodStr,
}
result, deliveryErr := xpack.DeliverTaskScanCustomWebhookAlertLog(alert, alert.Type, create, pushAlert, config, transport, agentInfo, task)
queued, err = result.Queued, deliveryErr
if err == nil && result.Queued {
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
}
} else {
err = xpack.AlertProvider.CreateTaskScanWebhookAlertLog(alert, alert.Type, create, pushAlert, config, transport, agentInfo)
}
err = xpack.AlertProvider.CreateTaskScanWebhookAlertLog(alert, alert.Type, create, pushAlert, config, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", alert.Type, methodStr, err)
return
}
if !queued {
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
}
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr)
}
}
-960
View File
@@ -1,960 +0,0 @@
package alert_webhook
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"unicode/utf8"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/google/uuid"
)
const (
MaskedSecret = "******"
DefaultGenericJSONTemplate = `{"schema_version":"1","title":"{{title}}","message":"{{message}}","type":"{{type}}","node_name":"{{nodeName}}","timestamp":"{{timestamp}}"}`
agentKeyPrefix = "agent:v1:"
maxDisplayNameRunes = 64
maxURLLength = 8192
maxHeaders = 64
maxHeaderNameLength = 256
maxHeaderValueLen = 16 * 1024
maxBodyLength = 256 * 1024
maxFormFields = 128
maxConfigLength = 512 * 1024
maxSecretLength = 256 * 1024
)
var supportedPresets = map[string]struct{}{
"genericJson": {},
"slack": {},
"discord": {},
"teamsWorkflows": {},
"custom": {},
}
var forbiddenHeaders = map[string]struct{}{
"connection": {},
"content-length": {},
"content-type": {},
"host": {},
"proxy-connection": {},
"proxy-authorization": {},
"te": {},
"trailer": {},
"transfer-encoding": {},
"upgrade": {},
}
var supportedTemplateVariables = map[string]struct{}{
"{{title}}": {},
"{{message}}": {},
"{{type}}": {},
"{{nodeName}}": {},
"{{timestamp}}": {},
}
type PreparedConfig struct {
Config string
SecretConfig string
DisplayName string
}
func Prepare(rawConfig, status string, existing *model.AlertConfig) (PreparedConfig, error) {
if len(rawConfig) > maxConfigLength {
return PreparedConfig{}, fmt.Errorf("custom webhook config exceeds %d bytes", maxConfigLength)
}
var mutation dto.AlertCustomWebhookConfig
if err := decodeStrict(rawConfig, &mutation); err != nil {
return PreparedConfig{}, fmt.Errorf("decode custom webhook config: %w", err)
}
if mutation.SchemaVersion != dto.AlertCustomWebhookSchemaVersion {
return PreparedConfig{}, fmt.Errorf("unsupported custom webhook schemaVersion: %d", mutation.SchemaVersion)
}
if mutation.State != "" {
return PreparedConfig{}, fmt.Errorf("custom webhook state is read-only")
}
existingSecret := dto.AlertCustomWebhookSecretConfig{
SchemaVersion: dto.AlertCustomWebhookSchemaVersion,
Headers: map[string]string{},
}
if existing != nil {
if existing.Type != constant.Custom {
return PreparedConfig{}, fmt.Errorf("alert config %d is not a custom webhook", existing.ID)
}
if mutationNeedsExistingSecret(mutation) {
var err error
existingSecret, err = decryptSecret(existing.SecretConfig)
if err != nil {
return PreparedConfig{}, err
}
}
}
stored, secret, err := mergeAndValidate(mutation, status, existingSecret, existing != nil)
if err != nil {
return PreparedConfig{}, err
}
configData, err := json.Marshal(stored)
if err != nil {
return PreparedConfig{}, fmt.Errorf("encode custom webhook config: %w", err)
}
secretData, err := json.Marshal(secret)
if err != nil {
return PreparedConfig{}, fmt.Errorf("encode custom webhook secret: %w", err)
}
if len(configData) > maxConfigLength {
return PreparedConfig{}, fmt.Errorf("custom webhook config exceeds %d bytes", maxConfigLength)
}
if len(secretData) > maxSecretLength {
return PreparedConfig{}, fmt.Errorf("custom webhook secret exceeds %d bytes", maxSecretLength)
}
secretCipher, err := encryptSecretPlain(string(secretData))
if err != nil {
return PreparedConfig{}, fmt.Errorf("encrypt custom webhook secret: %w", err)
}
return PreparedConfig{
Config: string(configData),
SecretConfig: secretCipher,
DisplayName: stored.DisplayName,
}, nil
}
func mutationNeedsExistingSecret(mutation dto.AlertCustomWebhookConfig) bool {
if mutation.URL.Action == "keep" {
return true
}
for _, header := range mutation.Headers {
if header.Secret && header.Action == "keep" {
return true
}
}
return false
}
type plainConfigView struct {
SchemaVersion int `json:"schemaVersion"`
State string `json:"state,omitempty"`
DisplayName string `json:"displayName"`
Preset string `json:"preset"`
Method string `json:"method"`
URL string `json:"url"`
Body dto.AlertCustomWebhookBody `json:"body"`
Headers []plainConfigViewHeader `json:"headers"`
}
type plainConfigViewHeader struct {
UID string `json:"uid"`
Key string `json:"key"`
Secret bool `json:"secret"`
Value string `json:"value"`
}
// PlainView decrypts a custom webhook only for the authenticated alert-config
// read contract. The encrypted SecretConfig remains the sole persisted copy.
func PlainView(config model.AlertConfig) (string, error) {
if config.Type != constant.Custom {
return config.Config, nil
}
stored, err := decodeStored(config.Config)
if err != nil {
if _, _, legacy := legacyValues(config.Config); legacy {
return config.Config, nil
}
return safeFallbackView(config, "", "invalid")
}
secret, err := validatedStoredSecret(config, stored)
if err != nil {
return safeFallbackView(config, stored.DisplayName, "invalid")
}
view := plainConfigView{
SchemaVersion: stored.SchemaVersion,
DisplayName: stored.DisplayName,
Preset: stored.Preset,
Method: stored.Method,
URL: secret.URL,
Body: stored.Body,
Headers: make([]plainConfigViewHeader, 0, len(stored.Headers)),
}
for _, header := range stored.Headers {
value := header.Value
if header.Secret {
value = secret.Headers[header.UID]
}
view.Headers = append(view.Headers, plainConfigViewHeader{
UID: header.UID,
Key: header.Key,
Secret: header.Secret,
Value: value,
})
}
result, err := json.Marshal(view)
if err != nil {
return "", fmt.Errorf("encode custom webhook plain view: %w", err)
}
return string(result), nil
}
func NormalizeLegacy(rawConfig, status, fallbackName string) (PreparedConfig, string, bool, error) {
displayName, rawURL, legacy := legacyValues(rawConfig)
if !legacy {
return PreparedConfig{}, status, false, nil
}
if displayName == "" {
displayName = strings.TrimSpace(fallbackName)
}
if displayName == "" || utf8.RuneCountInString(displayName) > maxDisplayNameRunes {
displayName = "Custom Webhook"
}
if status != constant.AlertEnable && status != constant.AlertDisable {
status = constant.AlertDisable
}
urlMutation := dto.AlertCustomWebhookSecretMutation{Action: "clear"}
if rawURL != "" {
if err := validateURL(rawURL); err == nil {
urlMutation = dto.AlertCustomWebhookSecretMutation{Action: "replace", Value: rawURL}
} else {
status = constant.AlertDisable
}
} else {
status = constant.AlertDisable
}
mutation := dto.AlertCustomWebhookConfig{
SchemaVersion: dto.AlertCustomWebhookSchemaVersion,
DisplayName: displayName,
Preset: "genericJson",
Method: http.MethodPost,
URL: dto.AlertCustomWebhookURL{AlertCustomWebhookSecretMutation: urlMutation},
Body: dto.AlertCustomWebhookBody{
Type: "json",
Template: DefaultGenericJSONTemplate,
},
Headers: make([]dto.AlertCustomWebhookHeader, 0),
}
data, err := json.Marshal(mutation)
if err != nil {
return PreparedConfig{}, status, true, fmt.Errorf("encode legacy custom webhook config: %w", err)
}
prepared, err := Prepare(string(data), status, nil)
return prepared, status, true, err
}
func safeFallbackView(config model.AlertConfig, displayName, state string) (string, error) {
if displayName == "" {
displayName = strings.TrimSpace(config.Title)
}
if displayName == "" || utf8.RuneCountInString(displayName) > maxDisplayNameRunes {
displayName = "Custom Webhook"
}
view := dto.AlertCustomWebhookConfig{
SchemaVersion: dto.AlertCustomWebhookSchemaVersion,
State: state,
DisplayName: displayName,
Preset: "genericJson",
Method: http.MethodPost,
URL: dto.AlertCustomWebhookURL{Configured: false},
Body: dto.AlertCustomWebhookBody{
Type: "json",
Template: DefaultGenericJSONTemplate,
},
Headers: make([]dto.AlertCustomWebhookHeader, 0),
}
result, err := json.Marshal(view)
if err != nil {
return "", fmt.Errorf("encode custom webhook fallback view: %w", err)
}
return string(result), nil
}
func legacyValues(rawConfig string) (string, string, bool) {
var raw map[string]json.RawMessage
if err := json.Unmarshal([]byte(rawConfig), &raw); err != nil || raw == nil || len(raw) != 2 {
return "", "", false
}
displayRaw, hasDisplayName := raw["displayName"]
urlRaw, hasURL := raw["url"]
if !hasDisplayName || !hasURL {
return "", "", false
}
var displayName, rawURL string
if err := json.Unmarshal(displayRaw, &displayName); err != nil {
return "", "", false
}
if err := json.Unmarshal(urlRaw, &rawURL); err != nil {
return "", "", false
}
return strings.TrimSpace(displayName), strings.TrimSpace(rawURL), true
}
func Resolve(config model.AlertConfig) (dto.AlertCustomWebhookResolvedConfig, error) {
if config.Type != constant.Custom {
return dto.AlertCustomWebhookResolvedConfig{}, fmt.Errorf("alert config %d is not a custom webhook", config.ID)
}
stored, err := decodeStored(config.Config)
if err != nil {
return dto.AlertCustomWebhookResolvedConfig{}, err
}
secret, err := decryptSecret(config.SecretConfig)
if err != nil {
return dto.AlertCustomWebhookResolvedConfig{}, err
}
if err := validateURL(secret.URL); err != nil {
return dto.AlertCustomWebhookResolvedConfig{}, err
}
resolved := dto.AlertCustomWebhookResolvedConfig{
SchemaVersion: stored.SchemaVersion,
DisplayName: stored.DisplayName,
Preset: stored.Preset,
Method: stored.Method,
URL: secret.URL,
Body: stored.Body,
Headers: make([]dto.AlertCustomWebhookResolvedHeader, 0, len(stored.Headers)),
}
for _, header := range stored.Headers {
value := header.Value
if header.Secret {
value = secret.Headers[header.UID]
if value == "" && !header.Configured {
continue
}
if value == "" {
return dto.AlertCustomWebhookResolvedConfig{}, fmt.Errorf("secret header %q is not configured", header.Key)
}
}
resolved.Headers = append(resolved.Headers, dto.AlertCustomWebhookResolvedHeader{Key: header.Key, Value: value})
}
return resolved, nil
}
func ValidateStored(config model.AlertConfig) error {
if config.Type != constant.Custom {
return fmt.Errorf("alert config %d is not a custom webhook", config.ID)
}
stored, err := decodeStored(config.Config)
if err != nil {
return err
}
return validateStoredSecret(config, stored)
}
func ExportSecretPlain(config model.AlertConfig) (string, error) {
if config.Type != constant.Custom {
return "", fmt.Errorf("alert config %d is not a custom webhook", config.ID)
}
plainText, err := decryptSecretPlain(config.SecretConfig)
if err != nil {
return "", err
}
secret, err := decodeAndValidateSecret(plainText)
if err != nil {
return "", err
}
canonical, err := json.Marshal(secret)
if err != nil {
return "", fmt.Errorf("encode custom webhook secret: %w", err)
}
return string(canonical), nil
}
func ImportSecretPlain(plainText string) (string, error) {
secret, err := decodeAndValidateSecret(plainText)
if err != nil {
return "", err
}
canonical, err := json.Marshal(secret)
if err != nil {
return "", fmt.Errorf("encode custom webhook secret: %w", err)
}
return encryptSecretPlain(string(canonical))
}
func ReencryptSecret(cipherText string) (string, error) {
plainText, err := decryptSecretPlain(cipherText)
if err != nil {
return "", err
}
return ImportSecretPlain(plainText)
}
func mergeAndValidate(
mutation dto.AlertCustomWebhookConfig,
status string,
existingSecret dto.AlertCustomWebhookSecretConfig,
hasExisting bool,
) (dto.AlertCustomWebhookConfig, dto.AlertCustomWebhookSecretConfig, error) {
mutation.DisplayName = strings.TrimSpace(mutation.DisplayName)
mutation.Preset = strings.TrimSpace(mutation.Preset)
if mutation.DisplayName == "" {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook displayName is required")
}
if utf8.RuneCountInString(mutation.DisplayName) > maxDisplayNameRunes {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook displayName exceeds %d characters", maxDisplayNameRunes)
}
if _, ok := supportedPresets[mutation.Preset]; !ok {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("unsupported custom webhook preset: %s", mutation.Preset)
}
if mutation.Method != http.MethodPost {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook method must be POST")
}
if err := validateBody(&mutation); err != nil {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, err
}
secret := dto.AlertCustomWebhookSecretConfig{
SchemaVersion: dto.AlertCustomWebhookSchemaVersion,
Headers: make(map[string]string),
}
switch mutation.URL.Action {
case "keep":
if !hasExisting || existingSecret.URL == "" {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook URL cannot be kept because it is not configured")
}
secret.URL = existingSecret.URL
case "replace":
secret.URL = strings.TrimSpace(mutation.URL.Value)
if err := validateURL(secret.URL); err != nil {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, err
}
case "clear":
secret.URL = ""
default:
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook URL action must be keep, replace, or clear")
}
if secret.URL == "" && status != constant.AlertDisable {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook URL is required while the config is enabled")
}
mutation.URL = dto.AlertCustomWebhookURL{Configured: secret.URL != ""}
if len(mutation.Headers) > maxHeaders {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook headers exceed the limit of %d", maxHeaders)
}
seenUIDs := make(map[string]struct{}, len(mutation.Headers))
seenKeys := make(map[string]struct{}, len(mutation.Headers))
storedHeaders := make([]dto.AlertCustomWebhookHeader, 0, len(mutation.Headers))
for _, header := range mutation.Headers {
header.UID = strings.TrimSpace(header.UID)
if header.UID == "" {
header.UID = uuid.NewString()
} else if _, err := uuid.Parse(header.UID); err != nil {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook header uid must be a UUID")
}
if _, exists := seenUIDs[header.UID]; exists {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("duplicate custom webhook header uid: %s", header.UID)
}
seenUIDs[header.UID] = struct{}{}
header.Key = http.CanonicalHeaderKey(strings.TrimSpace(header.Key))
if err := validateHeaderName(header.Key); err != nil {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, err
}
if isSensitiveHeaderName(header.Key) && !header.Secret {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook header %q must be marked secret", header.Key)
}
keyIdentity := strings.ToLower(header.Key)
if _, exists := seenKeys[keyIdentity]; exists {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("duplicate custom webhook header: %s", header.Key)
}
seenKeys[keyIdentity] = struct{}{}
storedHeader := dto.AlertCustomWebhookHeader{UID: header.UID, Key: header.Key, Secret: header.Secret}
if header.Secret {
switch header.Action {
case "keep":
value, ok := existingSecret.Headers[header.UID]
if !hasExisting || !ok || value == "" {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("secret header %q cannot be kept because it is not configured", header.Key)
}
secret.Headers[header.UID] = value
case "replace":
if header.Value == "" {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("secret header %q value is required", header.Key)
}
if err := validateHeaderValue(header.Value); err != nil {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("header %q value is invalid", header.Key)
}
secret.Headers[header.UID] = header.Value
case "clear":
default:
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("secret header %q action must be keep, replace, or clear", header.Key)
}
storedHeader.Configured = secret.Headers[header.UID] != ""
} else {
if header.Action != "replace" {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("non-secret header %q action must be replace", header.Key)
}
if err := validateHeaderValue(header.Value); err != nil {
return dto.AlertCustomWebhookConfig{}, dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("header %q value is invalid", header.Key)
}
storedHeader.Value = header.Value
}
storedHeaders = append(storedHeaders, storedHeader)
}
mutation.Headers = storedHeaders
mutation.SchemaVersion = dto.AlertCustomWebhookSchemaVersion
return mutation, secret, nil
}
func validateStoredSecret(config model.AlertConfig, stored dto.AlertCustomWebhookConfig) error {
_, err := validatedStoredSecret(config, stored)
return err
}
func validatedStoredSecret(config model.AlertConfig, stored dto.AlertCustomWebhookConfig) (dto.AlertCustomWebhookSecretConfig, error) {
if config.Status != constant.AlertEnable && config.Status != constant.AlertDisable {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("stored custom webhook status is invalid")
}
if strings.TrimSpace(config.SecretConfig) == "" {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("stored custom webhook secret is missing")
}
secret, err := decryptSecret(config.SecretConfig)
if err != nil {
return dto.AlertCustomWebhookSecretConfig{}, err
}
if secret.URL != "" {
if err := validateURL(secret.URL); err != nil {
return dto.AlertCustomWebhookSecretConfig{}, err
}
}
if stored.URL.Configured != (secret.URL != "") {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("stored custom webhook URL state does not match its secret")
}
if config.Status == constant.AlertEnable && secret.URL == "" {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook URL is required while the config is enabled")
}
usedSecrets := make(map[string]struct{}, len(stored.Headers))
for _, header := range stored.Headers {
if !header.Secret {
continue
}
value, configured := secret.Headers[header.UID]
if header.Configured != (configured && value != "") {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("stored custom webhook secret header state is inconsistent")
}
if configured {
usedSecrets[header.UID] = struct{}{}
}
}
if len(usedSecrets) != len(secret.Headers) {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("stored custom webhook contains orphaned header secrets")
}
return secret, nil
}
func validateBody(config *dto.AlertCustomWebhookConfig) error {
config.Body.Type = strings.TrimSpace(config.Body.Type)
if config.Preset != "custom" && config.Body.Type != "json" {
return fmt.Errorf("custom webhook preset %s requires a JSON body", config.Preset)
}
switch config.Body.Type {
case "json":
if config.Body.Template == "" {
return fmt.Errorf("json custom webhook body template is required")
}
if len(config.Body.Template) > maxBodyLength {
return fmt.Errorf("custom webhook body exceeds %d bytes", maxBodyLength)
}
if !json.Valid([]byte(config.Body.Template)) {
return fmt.Errorf("custom webhook JSON body template must be valid JSON")
}
var document any
if err := json.Unmarshal([]byte(config.Body.Template), &document); err != nil {
return fmt.Errorf("custom webhook JSON body template must be valid JSON")
}
if containsTemplateJSONKey(document) {
return fmt.Errorf("custom webhook JSON body keys cannot contain template variables")
}
if err := validateTemplateVariables(config.Body.Template); err != nil {
return err
}
if len(config.Body.Fields) != 0 {
return fmt.Errorf("json custom webhook body does not accept form fields")
}
case "form":
if config.Body.Template != "" {
return fmt.Errorf("form custom webhook body does not accept a template")
}
if len(config.Body.Fields) == 0 {
return fmt.Errorf("form custom webhook body requires at least one field")
}
if len(config.Body.Fields) > maxFormFields {
return fmt.Errorf("custom webhook form fields exceed the limit of %d", maxFormFields)
}
seen := make(map[string]struct{}, len(config.Body.Fields))
for index := range config.Body.Fields {
field := &config.Body.Fields[index]
field.Key = strings.TrimSpace(field.Key)
if field.Key == "" {
return fmt.Errorf("custom webhook form field key is required")
}
if strings.Contains(field.Key, "{{") || strings.Contains(field.Key, "}}") {
return fmt.Errorf("custom webhook form field keys cannot contain template variables")
}
if len(field.Key) > maxHeaderNameLength || len(field.Value) > maxHeaderValueLen {
return fmt.Errorf("custom webhook form field %q is too long", field.Key)
}
identity := strings.ToLower(field.Key)
if _, exists := seen[identity]; exists {
return fmt.Errorf("duplicate custom webhook form field: %s", field.Key)
}
seen[identity] = struct{}{}
if err := validateTemplateVariables(field.Value); err != nil {
return err
}
}
case "text":
if config.Body.Template == "" {
return fmt.Errorf("text custom webhook body template is required")
}
if len(config.Body.Template) > maxBodyLength {
return fmt.Errorf("custom webhook body exceeds %d bytes", maxBodyLength)
}
if err := validateTemplateVariables(config.Body.Template); err != nil {
return err
}
if len(config.Body.Fields) != 0 {
return fmt.Errorf("text custom webhook body does not accept form fields")
}
default:
return fmt.Errorf("custom webhook body type must be json, form, or text")
}
return nil
}
func containsTemplateJSONKey(value any) bool {
switch typed := value.(type) {
case map[string]any:
for key, child := range typed {
if strings.Contains(key, "{{") || strings.Contains(key, "}}") || containsTemplateJSONKey(child) {
return true
}
}
case []any:
for _, child := range typed {
if containsTemplateJSONKey(child) {
return true
}
}
}
return false
}
func validateTemplateVariables(template string) error {
remainder := template
for {
start := strings.Index(remainder, "{{")
if start < 0 {
if strings.Contains(remainder, "}}") {
return fmt.Errorf("custom webhook body contains a malformed template variable")
}
return nil
}
if strings.Contains(remainder[:start], "}}") {
return fmt.Errorf("custom webhook body contains a malformed template variable")
}
endOffset := strings.Index(remainder[start+2:], "}}")
if endOffset < 0 {
return fmt.Errorf("custom webhook body contains a malformed template variable")
}
end := start + 2 + endOffset + 2
variable := remainder[start:end]
if _, supported := supportedTemplateVariables[variable]; !supported {
return fmt.Errorf("custom webhook body contains an unsupported template variable")
}
remainder = remainder[end:]
}
}
func ContentTypeForBodyType(bodyType string) (string, error) {
switch bodyType {
case "json":
return "application/json", nil
case "form":
return "application/x-www-form-urlencoded", nil
case "text":
return "text/plain", nil
default:
return "", fmt.Errorf("custom webhook body type must be json, form, or text")
}
}
func validateURL(rawURL string) error {
if rawURL == "" {
return fmt.Errorf("custom webhook URL is required")
}
if len(rawURL) > maxURLLength {
return fmt.Errorf("custom webhook URL exceeds %d bytes", maxURLLength)
}
parsed, err := url.Parse(strings.TrimSpace(rawURL))
if err != nil {
return fmt.Errorf("invalid custom webhook URL")
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return fmt.Errorf("custom webhook URL scheme must be http or https")
}
hostname := strings.TrimSuffix(strings.TrimSpace(parsed.Hostname()), ".")
if parsed.Host == "" || hostname == "" || strings.Contains(hostname, "%") {
return fmt.Errorf("custom webhook URL host is required")
}
if strings.HasSuffix(parsed.Host, ":") {
return fmt.Errorf("custom webhook URL port is invalid")
}
if port := parsed.Port(); port != "" {
value, err := strconv.Atoi(port)
if err != nil || value < 1 || value > 65535 {
return fmt.Errorf("custom webhook URL port is invalid")
}
}
if parsed.User != nil {
return fmt.Errorf("custom webhook URL must not contain user info")
}
if parsed.Fragment != "" {
return fmt.Errorf("custom webhook URL must not contain a fragment")
}
return nil
}
func validateHeaderName(name string) error {
if name == "" {
return fmt.Errorf("custom webhook header name is required")
}
if len(name) > maxHeaderNameLength {
return fmt.Errorf("custom webhook header name is too long")
}
for index := 0; index < len(name); index++ {
if !isHeaderTokenByte(name[index]) {
return fmt.Errorf("invalid custom webhook header name: %s", name)
}
}
if _, forbidden := forbiddenHeaders[strings.ToLower(name)]; forbidden {
return fmt.Errorf("custom webhook header %q is managed by the sender", name)
}
return nil
}
func validateHeaderValue(value string) error {
if len(value) > maxHeaderValueLen || strings.ContainsAny(value, "\r\n") {
return fmt.Errorf("invalid custom webhook header value")
}
return nil
}
func isSensitiveHeaderName(name string) bool {
normalized := strings.ToLower(strings.TrimSpace(name))
compact := strings.NewReplacer("-", "", "_", "").Replace(normalized)
return normalized == "authorization" ||
normalized == "cookie" ||
strings.Contains(compact, "token") ||
strings.Contains(compact, "secret") ||
strings.Contains(compact, "signature") ||
strings.Contains(compact, "apikey") ||
strings.HasSuffix(normalized, "-key") ||
strings.HasSuffix(normalized, "_key")
}
func isHeaderTokenByte(char byte) bool {
if char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' {
return true
}
return strings.ContainsRune("!#$%&'*+-.^_`|~", rune(char))
}
func decodeStored(rawConfig string) (dto.AlertCustomWebhookConfig, error) {
if len(rawConfig) > maxConfigLength {
return dto.AlertCustomWebhookConfig{}, fmt.Errorf("stored custom webhook config exceeds %d bytes", maxConfigLength)
}
var stored dto.AlertCustomWebhookConfig
if err := decodeStrict(rawConfig, &stored); err != nil {
return dto.AlertCustomWebhookConfig{}, fmt.Errorf("decode stored custom webhook config: %w", err)
}
if stored.SchemaVersion != dto.AlertCustomWebhookSchemaVersion {
return dto.AlertCustomWebhookConfig{}, fmt.Errorf("unsupported stored custom webhook schemaVersion: %d", stored.SchemaVersion)
}
if stored.State != "" {
return dto.AlertCustomWebhookConfig{}, fmt.Errorf("stored custom webhook contains a view state")
}
if stored.URL.Action != "" || stored.URL.Value != "" {
return dto.AlertCustomWebhookConfig{}, fmt.Errorf("stored custom webhook URL contains a mutation")
}
for _, header := range stored.Headers {
if header.Action != "" || header.Secret && header.Value != "" {
return dto.AlertCustomWebhookConfig{}, fmt.Errorf("stored custom webhook header %q contains a mutation", header.Key)
}
}
if err := validateStoredConfig(&stored); err != nil {
return dto.AlertCustomWebhookConfig{}, err
}
return stored, nil
}
func validateStoredConfig(config *dto.AlertCustomWebhookConfig) error {
config.DisplayName = strings.TrimSpace(config.DisplayName)
if config.DisplayName == "" || utf8.RuneCountInString(config.DisplayName) > maxDisplayNameRunes {
return fmt.Errorf("stored custom webhook displayName is invalid")
}
if _, ok := supportedPresets[config.Preset]; !ok {
return fmt.Errorf("unsupported stored custom webhook preset: %s", config.Preset)
}
if config.Method != http.MethodPost {
return fmt.Errorf("stored custom webhook method must be POST")
}
if err := validateBody(config); err != nil {
return err
}
seenUIDs := make(map[string]struct{}, len(config.Headers))
seenKeys := make(map[string]struct{}, len(config.Headers))
for index := range config.Headers {
header := &config.Headers[index]
if header.UID == "" {
return fmt.Errorf("stored custom webhook header uid is required")
}
if _, err := uuid.Parse(header.UID); err != nil {
return fmt.Errorf("stored custom webhook header uid must be a UUID")
}
if _, exists := seenUIDs[header.UID]; exists {
return fmt.Errorf("duplicate stored custom webhook header uid: %s", header.UID)
}
seenUIDs[header.UID] = struct{}{}
header.Key = http.CanonicalHeaderKey(strings.TrimSpace(header.Key))
if err := validateHeaderName(header.Key); err != nil {
return err
}
if isSensitiveHeaderName(header.Key) && !header.Secret {
return fmt.Errorf("stored custom webhook header %q must be marked secret", header.Key)
}
identity := strings.ToLower(header.Key)
if _, exists := seenKeys[identity]; exists {
return fmt.Errorf("duplicate stored custom webhook header: %s", header.Key)
}
seenKeys[identity] = struct{}{}
if !header.Secret {
if err := validateHeaderValue(header.Value); err != nil {
return fmt.Errorf("stored header %q value is invalid", header.Key)
}
}
}
return nil
}
func decryptSecret(cipherText string) (dto.AlertCustomWebhookSecretConfig, error) {
if strings.TrimSpace(cipherText) == "" {
return dto.AlertCustomWebhookSecretConfig{
SchemaVersion: dto.AlertCustomWebhookSchemaVersion,
Headers: map[string]string{},
}, nil
}
plainText, err := decryptSecretPlain(cipherText)
if err != nil {
return dto.AlertCustomWebhookSecretConfig{}, err
}
return decodeAndValidateSecret(plainText)
}
func encryptSecretPlain(plainText string) (string, error) {
key, err := loadAgentEncryptKey()
if err != nil {
return "", err
}
cipherText, err := encrypt.StringEncryptWithKey(plainText, key)
if err != nil {
return "", fmt.Errorf("encrypt custom webhook secret with agent key: %w", err)
}
return agentKeyPrefix + cipherText, nil
}
func decryptSecretPlain(cipherText string) (string, error) {
if !strings.HasPrefix(cipherText, agentKeyPrefix) {
return "", fmt.Errorf("unsupported custom webhook secret format")
}
payload := strings.TrimPrefix(cipherText, agentKeyPrefix)
if payload == "" {
return "", fmt.Errorf("decrypt custom webhook secret: ciphertext is empty")
}
key, err := loadAgentEncryptKey()
if err != nil {
return "", err
}
plainText, err := encrypt.StringDecryptWithKey(payload, key)
if err != nil {
return "", fmt.Errorf("decrypt custom webhook secret: %w", err)
}
return plainText, nil
}
func loadAgentEncryptKey() (string, error) {
if key := strings.TrimSpace(global.CONF.Base.EncryptKey); key != "" {
return key, nil
}
if global.DB != nil {
var setting model.Setting
if err := global.DB.Where("key = ?", "EncryptKey").First(&setting).Error; err == nil {
if key := strings.TrimSpace(setting.Value); key != "" {
return key, nil
}
}
}
return "", fmt.Errorf("custom webhook agent encrypt key is empty")
}
func decodeAndValidateSecret(plainText string) (dto.AlertCustomWebhookSecretConfig, error) {
if len(plainText) > maxSecretLength {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook secret exceeds %d bytes", maxSecretLength)
}
var secret dto.AlertCustomWebhookSecretConfig
if err := decodeStrict(plainText, &secret); err != nil {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("decode custom webhook secret: %w", err)
}
if secret.SchemaVersion != dto.AlertCustomWebhookSchemaVersion {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("unsupported custom webhook secret schemaVersion: %d", secret.SchemaVersion)
}
if secret.URL != "" {
if err := validateURL(secret.URL); err != nil {
return dto.AlertCustomWebhookSecretConfig{}, err
}
}
if secret.Headers == nil {
secret.Headers = map[string]string{}
}
if len(secret.Headers) > maxHeaders {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook secret headers exceed the limit of %d", maxHeaders)
}
for uid, value := range secret.Headers {
if _, err := uuid.Parse(strings.TrimSpace(uid)); err != nil {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook secret header uid must be a UUID")
}
if value == "" {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook secret header %q is empty", uid)
}
if err := validateHeaderValue(value); err != nil {
return dto.AlertCustomWebhookSecretConfig{}, fmt.Errorf("custom webhook secret header %q is invalid", uid)
}
}
return secret, nil
}
func decodeStrict(raw string, target any) error {
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return err
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
if err == nil {
return fmt.Errorf("multiple JSON values are not allowed")
}
return err
}
return nil
}

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