From 45d7b30abccc07c27f4cacf75fa3c2dc23203441 Mon Sep 17 00:00:00 2001 From: CityFun <31820853+zhengkunwang223@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:00:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Add=20support=20for=20syncing=20self-si?= =?UTF-8?q?gned=20certificates=20and=20manually=20u=E2=80=A6=20(#13068)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add support for syncing self-signed certificates and manually uploaded certificates to other nodes. * feat: Add support for syncing self-signed certificates and manually --- agent/app/api/v2/website_ssl.go | 25 ++++ agent/app/dto/request/website_ssl.go | 13 ++ agent/app/service/website_ca.go | 6 + agent/app/service/website_ssl.go | 131 +++++++++++++++--- agent/router/ro_website_ssl.go | 1 + frontend/src/api/interface/website.ts | 14 ++ frontend/src/api/modules/website.ts | 4 + .../src/views/website/ssl/ca/obtain/index.vue | 27 +++- frontend/src/views/website/ssl/index.vue | 131 +++++++++++++++++- .../src/views/website/ssl/upload/index.vue | 36 ++++- 10 files changed, 367 insertions(+), 21 deletions(-) diff --git a/agent/app/api/v2/website_ssl.go b/agent/app/api/v2/website_ssl.go index 2fd3bf098..a32104d96 100644 --- a/agent/app/api/v2/website_ssl.go +++ b/agent/app/api/v2/website_ssl.go @@ -210,6 +210,27 @@ func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) { helper.Success(c) } +// @Tags Website SSL +// @Summary Push ssl to nodes +// @Accept json +// @Param request body request.WebsiteSSLPush true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/ssl/push [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"推送证书到节点 [domain]","formatEN":"Push ssl to nodes [domain]"} +func (b *BaseApi) PushWebsiteSSLToNode(c *gin.Context) { + var req request.WebsiteSSLPush + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteSSLService.PushToNode(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + // @Tags Website SSL // @Summary Upload ssl // @Accept json @@ -248,6 +269,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) { var req request.WebsiteSSLFileUpload req.Description = c.PostForm("description") + req.Nodes = c.PostForm("nodes") + req.PushNode, _ = strconv.ParseBool(c.PostForm("pushNode")) sslID := c.PostForm("sslID") if sslID != "" { req.SSLID, _ = strconv.ParseUint(sslID, 10, 64) @@ -283,6 +306,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) { Certificate: string(certificateContent), Description: req.Description, SSLID: uint(req.SSLID), + PushNode: req.PushNode, + Nodes: req.Nodes, } if err := websiteSSLService.Upload(uploadReq); err != nil { diff --git a/agent/app/dto/request/website_ssl.go b/agent/app/dto/request/website_ssl.go index 208d1ffd9..758321f00 100644 --- a/agent/app/dto/request/website_ssl.go +++ b/agent/app/dto/request/website_ssl.go @@ -126,6 +126,15 @@ type WebsiteSSLUpload struct { Type string `json:"type" validate:"required,oneof=paste local"` SSLID uint `json:"sslID"` Description string `json:"description"` + PushNode bool `json:"pushNode"` + Nodes string `json:"nodes"` +} + +type WebsiteSSLPush struct { + ID uint `json:"id" validate:"required"` + PushNode bool `json:"pushNode"` + Nodes string `json:"nodes"` + TaskID string `json:"taskID" validate:"required"` } type WebsiteCASearch struct { @@ -157,6 +166,8 @@ type WebsiteCAObtain struct { Description string `json:"description"` ExecShell bool `json:"execShell"` Shell string `json:"shell"` + PushNode bool `json:"pushNode"` + Nodes string `json:"nodes"` } type WebsiteCARenew struct { @@ -167,4 +178,6 @@ type WebsiteSSLFileUpload struct { Type string `json:"type"` Description string `json:"description"` SSLID uint64 `json:"sslID"` + PushNode bool `json:"pushNode"` + Nodes string `json:"nodes"` } diff --git a/agent/app/service/website_ca.go b/agent/app/service/website_ca.go index 6b8ffa090..0052e7fac 100644 --- a/agent/app/service/website_ca.go +++ b/agent/app/service/website_ca.go @@ -207,6 +207,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website Description: req.Description, ExecShell: req.ExecShell, } + setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes) if req.ExecShell { websiteSSL.Shell = req.Shell } @@ -384,6 +385,11 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website } } reloadSystemSSL(websiteSSL, logger) + if websiteSSL.PushNode { + if err = pushSSLToNode(websiteSSL, logger); err != nil { + return nil, err + } + } return websiteSSL, nil } diff --git a/agent/app/service/website_ssl.go b/agent/app/service/website_ssl.go index 3a958f8e8..0a6da282b 100644 --- a/agent/app/service/website_ssl.go +++ b/agent/app/service/website_ssl.go @@ -24,6 +24,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/app/dto/response" "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" @@ -104,6 +105,7 @@ type IWebsiteSSLService interface { Delete(ids []uint) error Update(update request.WebsiteSSLUpdate) error Upload(req request.WebsiteSSLUpload) error + PushToNode(req request.WebsiteSSLPush) error ObtainSSL(apply request.WebsiteSSLApply) error AutoRenewSSL(id uint) error SyncForRestart() error @@ -214,10 +216,7 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs } websiteSSL.Dir = create.Dir } - if create.PushNode && global.IsMaster && len(create.Nodes) > 0 { - websiteSSL.PushNode = true - websiteSSL.Nodes = create.Nodes - } + setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes) var domains []string if create.OtherDomains != "" { @@ -282,6 +281,47 @@ func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{ logger.Println(i18n.GetMsgWithMap(msgKey, params)) } +func normalizeSSLPushConfig(pushNode bool, nodes string) (bool, string) { + nodes = strings.TrimSpace(nodes) + if !pushNode || nodes == "" { + return false, "" + } + return true, nodes +} + +func setSSLPushConfig(websiteSSL *model.WebsiteSSL, pushNode bool, nodes string) { + pushNode, nodes = normalizeSSLPushConfig(pushNode, nodes) + if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() { + pushNode = false + nodes = "" + } + websiteSSL.PushNode = pushNode + websiteSSL.Nodes = nodes +} + +func pushSSLToNode(websiteSSL *model.WebsiteSSL, logger *log.Logger) error { + printSSLLog(logger, "StartPushSSLToNode", nil) + if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil { + printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()}) + return err + } + printSSLLog(logger, "PushSSLToNodeSuccess", nil) + return nil +} + +func pushSSLToNodeWithNewLogger(websiteSSL *model.WebsiteSSL) error { + if !websiteSSL.PushNode { + return nil + } + logFile, logger := newWebsiteSSLLogger(websiteSSL, false) + if logFile != nil { + defer func() { + _ = logFile.Close() + }() + } + return pushSSLToNode(websiteSSL, logger) +} + func newWebsiteSSLLogger(websiteSSL *model.WebsiteSSL, autoRenew bool) (*os.File, *log.Logger) { flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC if autoRenew { @@ -567,12 +607,9 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error { } reloadSystemSSL(websiteSSL, logger) if websiteSSL.PushNode { - printSSLLog(logger, "StartPushSSLToNode", nil) - if err = xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil { - printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()}) + if err = pushSSLToNode(websiteSSL, logger); err != nil { return } - printSSLLog(logger, "PushSSLToNodeSuccess", nil) } }(logFile, logger) @@ -740,13 +777,13 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error { } else { updateParams["shell"] = "" } - if update.PushNode { - updateParams["push_node"] = true - updateParams["nodes"] = update.Nodes - } else { - updateParams["push_node"] = false - updateParams["nodes"] = "" + pushNode, nodes := normalizeSSLPushConfig(update.PushNode, update.Nodes) + if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() { + pushNode = false + nodes = "" } + updateParams["push_node"] = pushNode + updateParams["nodes"] = nodes if websiteSSL.Provider != constant.SelfSigned && websiteSSL.Provider != constant.Manual { acmeAccount, err := websiteAcmeRepo.GetFirst(repo.WithByID(update.AcmeAccountID)) @@ -797,6 +834,7 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error { Description: req.Description, Status: constant.SSLReady, } + setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes) var err error if req.SSLID > 0 { websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID)) @@ -804,6 +842,7 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error { return err } websiteSSL.Description = req.Description + setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes) } if req.Type == "local" { fileOp := files.NewFileOp() @@ -891,9 +930,69 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error { if err := UpdateSSLConfig(*websiteSSL); err != nil { return err } - return websiteSSLRepo.Save(websiteSSL) + if err := websiteSSLRepo.Save(websiteSSL); err != nil { + return err + } + return pushSSLToNodeWithNewLogger(websiteSSL) } - return websiteSSLRepo.Create(context.Background(), websiteSSL) + if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil { + return err + } + return pushSSLToNodeWithNewLogger(websiteSSL) +} + +func (w WebsiteSSLService) PushToNode(req request.WebsiteSSLPush) error { + if !global.IsMaster { + return errors.New("only master node can push SSL to nodes") + } + if !xpack.MultiNodeProvider.IsXpack() { + return errors.New("SSL node push is an XPack feature") + } + pushNode, nodes := normalizeSSLPushConfig(req.PushNode, req.Nodes) + if !pushNode { + return errors.New("please select nodes to push SSL") + } + websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(req.ID)) + if err != nil { + return err + } + if websiteSSL.Provider == constant.FromMaster { + return errors.New("SSL imported from master node can not be pushed") + } + if websiteSSL.Status != constant.SSLReady { + return errors.New("only ready SSL can be pushed") + } + if task.CheckResourceTaskIsExecuting(task.TaskPush, task.TaskScopeWebsite, websiteSSL.ID) { + return buserr.New("TaskIsExecuting") + } + if err := websiteSSLRepo.SaveByMap(websiteSSL, map[string]interface{}{ + "push_node": pushNode, + "nodes": nodes, + }); err != nil { + return err + } + websiteSSL.PushNode = pushNode + websiteSSL.Nodes = nodes + + pushTask, err := task.NewTaskWithOps(websiteSSL.PrimaryDomain, task.TaskPush, task.TaskScopeWebsite, req.TaskID, websiteSSL.ID) + if err != nil { + return err + } + pushTask.AddSubTask(i18n.GetMsgByKey("StartPushSSLToNode"), func(t *task.Task) error { + t.Log(i18n.GetMsgByKey("StartPushSSLToNode")) + if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil { + t.Log(i18n.GetMsgWithMap("PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()})) + return err + } + t.Log(i18n.GetMsgByKey("PushSSLToNodeSuccess")) + return nil + }, nil) + go func() { + if err := pushTask.Execute(); err != nil { + global.LOG.Errorf("push ssl to node failed, sslID: %d, err: %v", websiteSSL.ID, err) + } + }() + return nil } func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) { diff --git a/agent/router/ro_website_ssl.go b/agent/router/ro_website_ssl.go index 28e42d2c9..7e9f1caaf 100644 --- a/agent/router/ro_website_ssl.go +++ b/agent/router/ro_website_ssl.go @@ -21,6 +21,7 @@ func (a *WebsiteSSLRouter) InitRouter(Router *gin.RouterGroup) { groupRouter.GET("/website/:websiteId", baseApi.GetWebsiteSSLByWebsiteId) groupRouter.GET("/:id", baseApi.GetWebsiteSSLById) groupRouter.POST("/update", baseApi.UpdateWebsiteSSL) + groupRouter.POST("/push", baseApi.PushWebsiteSSLToNode) groupRouter.POST("/upload", baseApi.UploadWebsiteSSL) groupRouter.POST("/obtain", baseApi.ApplyWebsiteSSL) groupRouter.POST("/download", baseApi.DownloadWebsiteSSL) diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index f1fcb4fde..9ec7445f7 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -295,6 +295,15 @@ export namespace Website { keyType: string; pushDir: boolean; dir: string; + pushNode?: boolean; + nodes?: string; + } + + export interface SSLPush { + id: number; + pushNode: boolean; + nodes: string; + taskID: string; } export interface AcmeAccount extends CommonModel { @@ -578,6 +587,9 @@ export namespace Website { certificatePath: string; type: string; sslID: number; + description?: string; + pushNode?: boolean; + nodes?: string; } export interface SSLObtain { @@ -620,6 +632,8 @@ export namespace Website { pushDir: boolean; dir: string; description: string; + pushNode?: boolean; + nodes?: string; } export interface RenewSSLByCA { diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index 94f1e388d..2335d8dc4 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -148,6 +148,10 @@ export const updateSSL = (req: Website.SSLUpdate) => { return http.post(`/websites/ssl/update`, req); }; +export const pushSSLToNode = (req: Website.SSLPush) => { + return http.post(`/websites/ssl/push`, req); +}; + export const getDnsResolve = (req: Website.DNSResolveReq) => { return http.post(`/websites/ssl/resolve`, req, TimeoutEnum.T_5M); }; diff --git a/frontend/src/views/website/ssl/ca/obtain/index.vue b/frontend/src/views/website/ssl/ca/obtain/index.vue index 3250fd29a..459e79ec1 100644 --- a/frontend/src/views/website/ssl/ca/obtain/index.vue +++ b/frontend/src/views/website/ssl/ca/obtain/index.vue @@ -62,6 +62,14 @@ {{ $t('ssl.shellHelper') }} + @@ -84,8 +92,20 @@ import i18n from '@/lang'; import FileList from '@/components/file-list/index.vue'; import { MsgSuccess } from '@/utils/message'; import { FormInstance } from 'element-plus'; -import { ref } from 'vue'; +import { defineAsyncComponent, ref } from 'vue'; import { KeyTypes } from '@/global/mimetype'; +import { useGlobalStore } from '@/composables/useGlobalStore'; + +const { isMaster, isXpackOrEE } = useGlobalStore(); + +const PushToNode = defineAsyncComponent(async () => { + const modules = import.meta.glob('@/xpack/views/ssl/index.vue'); + const loader = modules['/src/xpack/views/ssl/index.vue']; + if (loader) { + return ((await loader()) as any).default; + } + return { template: '
' }; +}); const open = ref(false); const fileRef = ref(); @@ -100,6 +120,7 @@ const rules = ref({ time: [Rules.integerNumber, checkNumberRange(1, 10000)], shell: [Rules.requiredInput], description: [checkMaxLength(128)], + pushNodes: [Rules.requiredSelect], }); const initData = () => ({ @@ -114,6 +135,9 @@ const initData = () => ({ description: '', execShell: false, shell: '', + pushNode: false, + pushNodes: [] as string[], + nodes: '', }); const obtain = ref(initData()); @@ -144,6 +168,7 @@ const submit = async (formEl: FormInstance | undefined) => { return; } loading.value = true; + obtain.value.nodes = obtain.value.pushNode ? obtain.value.pushNodes.join(',') : ''; obtainSSLByCA(obtain.value) .then(() => { diff --git a/frontend/src/views/website/ssl/index.vue b/frontend/src/views/website/ssl/index.vue index 97f34dbaf..330dba498 100644 --- a/frontend/src/views/website/ssl/index.vue +++ b/frontend/src/views/website/ssl/index.vue @@ -182,13 +182,47 @@ + + + + + + +