diff --git a/backend/app/api/access.go b/backend/app/api/access.go new file mode 100644 index 0000000..fecad4a --- /dev/null +++ b/backend/app/api/access.go @@ -0,0 +1,135 @@ +package api + +import ( + "ALLinSSL/backend/internal/access" + "ALLinSSL/backend/public" + "github.com/gin-gonic/gin" + "strings" +) + +func GetAccessList(c *gin.Context) { + var form struct { + Search string `form:"search"` + Page int64 `form:"p"` + Limit int64 `form:"limit"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + accessList, count, err := access.GetList(form.Search, form.Page, form.Limit) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessData(c, accessList, count) + return + +} + +func GetAllAccess(c *gin.Context) { + var form struct { + Type string `form:"type"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + accessList, err := access.GetAll(form.Type) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessData(c, accessList, 0) + return + +} + +func AddAccess(c *gin.Context) { + var form struct { + Name string `form:"name"` + Type string `form:"type"` + Config string `form:"config"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.Name = strings.TrimSpace(form.Name) + if form.Name == "" { + public.FailMsg(c, "名称不能为空") + return + } + if form.Type == "" { + public.FailMsg(c, "类型不能为空") + return + } + if form.Config == "" { + public.FailMsg(c, "配置不能为空") + return + } + err = access.AddAccess(form.Config, form.Name, form.Type) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "添加成功") + return + +} + +func UpdateAccess(c *gin.Context) { + var form struct { + ID string `form:"id"` + Name string `form:"name"` + Config string `form:"config"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.Name = strings.TrimSpace(form.Name) + if form.Name == "" { + public.FailMsg(c, "名称不能为空") + return + } + if form.Config == "" { + public.FailMsg(c, "配置不能为空") + return + } + err = access.UpdateAccess(form.ID, form.Config, form.Name) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "修改成功") + return + +} + +func DelAccess(c *gin.Context) { + var form struct { + ID string `form:"id"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.ID = strings.TrimSpace(form.ID) + if form.ID == "" { + public.FailMsg(c, "ID不能为空") + return + } + err = access.DelAccess(form.ID) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "删除成功") + return +} diff --git a/backend/app/api/cert.go b/backend/app/api/cert.go new file mode 100644 index 0000000..e435b2d --- /dev/null +++ b/backend/app/api/cert.go @@ -0,0 +1,129 @@ +package api + +import ( + "ALLinSSL/backend/internal/cert" + "ALLinSSL/backend/public" + "archive/zip" + "bytes" + "github.com/gin-gonic/gin" + "strings" +) + +func GetCertList(c *gin.Context) { + var form struct { + Search string `form:"search"` + Page int64 `form:"p"` + Limit int64 `form:"limit"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + certList, count, err := cert.GetList(form.Search, form.Page, form.Limit) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessData(c, certList, count) + return +} + +func UploadCert(c *gin.Context) { + var form struct { + Key string `form:"key"` + Cert string `form:"cert"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.Key = strings.TrimSpace(form.Key) + form.Cert = strings.TrimSpace(form.Cert) + + if form.Key == "" { + public.FailMsg(c, "名称不能为空") + return + } + if form.Cert == "" { + public.FailMsg(c, "类型不能为空") + return + } + err = cert.UploadCert(form.Key, form.Cert) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "添加成功") + return +} + +func DelCert(c *gin.Context) { + var form struct { + ID string `form:"id"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + if form.ID == "" { + public.FailMsg(c, "ID不能为空") + return + } + err = cert.DelCert(form.ID) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "删除成功") + return +} + +func DownloadCert(c *gin.Context) { + ID := c.Query("id") + + if ID == "" { + public.FailMsg(c, "ID不能为空") + return + } + certData, err := cert.GetCert(ID) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + + // 构建 zip 包(内存中) + buf := new(bytes.Buffer) + zipWriter := zip.NewWriter(buf) + + for filename, content := range certData { + if filename == "cert" || filename == "key" { + writer, err := zipWriter.Create(filename + ".pem") + if err != nil { + public.FailMsg(c, err.Error()) + return + } + _, err = writer.Write([]byte(content)) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + } + } + // 关闭 zipWriter + if err := zipWriter.Close(); err != nil { + public.FailMsg(c, err.Error()) + return + } + // 设置响应头 + + zipName := strings.ReplaceAll(certData["domains"], ".", "_") + zipName = strings.ReplaceAll(zipName, ",", "-") + + c.Header("Content-Type", "application/zip") + c.Header("Content-Disposition", "attachment; filename="+zipName+".zip") + c.Data(200, "application/zip", buf.Bytes()) + return +} diff --git a/backend/app/api/login.go b/backend/app/api/login.go new file mode 100644 index 0000000..2a89917 --- /dev/null +++ b/backend/app/api/login.go @@ -0,0 +1,162 @@ +package api + +import ( + "ALLinSSL/backend/public" + "crypto/md5" + "encoding/hex" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "strings" + "time" +) + +func Sign(c *gin.Context) { + var form struct { + Username string `form:"username" binding:"required"` + Password string `form:"password" binding:"required"` + Code string `form:"code"` + } + err := c.Bind(&form) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + // return + } + form.Username = strings.TrimSpace(form.Username) + form.Code = strings.TrimSpace(form.Code) + + // 从数据库拿用户 + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + s.Connect() + defer s.Close() + s.TableName = "users" + res, err := s.Where("username=?", []interface{}{form.Username}).Select() + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + + session := sessions.Default(c) + now := time.Now() + + loginErrCount := session.Get("__loginErrCount") + loginErrEnd := session.Get("__loginErrEnd") + ErrCount := 0 + ErrEnd := now + // 获取登录错误次数 + if __loginErrCount, ok := loginErrCount.(int); ok { + ErrCount = __loginErrCount + } + // 获取登录错误时间 + if __loginErrEnd, ok := loginErrEnd.(time.Time); ok { + ErrEnd = __loginErrEnd + } + + // fmt.Println(ErrCount, ErrEnd) + + // 判断登录错误次数 + switch { + case ErrCount >= 5: + // 登录错误次数超过5次,15分钟内禁止登录 + if now.Sub(ErrEnd) < 15*time.Minute { + // c.JSON(http.StatusBadRequest, public.ResERR("登录次数过多,请15分钟后再试")) + public.FailMsg(c, "登录次数过多,请15分钟后再试") + return + } + session.Delete("__loginErrEnd") + case ErrCount > 0: + if form.Code == "" { + // c.JSON(http.StatusBadRequest, public.ResERR("验证码错误1")) + public.FailMsg(c, "验证码错误1") + return + } else { + // 这里添加验证码的逻辑 + verifyCode := session.Get("_verifyCode") + if _verifyCode, ok := verifyCode.(string); ok { + if !strings.EqualFold(form.Code, _verifyCode) { + // c.JSON(http.StatusBadRequest, public.ResERR("验证码错误2")) + public.FailMsg(c, "验证码错误2") + return + } + } else { + // c.JSON(http.StatusBadRequest, public.ResERR("验证码错误3")) + public.FailMsg(c, "验证码错误3") + return + } + } + } + + // 判断用户是否存在 + if len(res) == 0 { + session.Set("__loginErrCount", ErrCount+1) + session.Set("__loginErrEnd", now) + _ = session.Save() + // c.JSON(http.StatusBadRequest, public.ResERR("用户不存在")) + // 设置cookie + c.SetCookie("must_code", "1", 0, "/", "", false, true) + public.FailMsg(c, "用户不存在") + return + } + // 判断密码是否正确 + // qSalt := "_bt_all_in_ssl" + // password := md5.Sum([]byte(form.Password + qSalt)) + // passwordMd5 := hex.EncodeToString(password[:]) + // fmt.Println(passwordMd5) + salt, ok := res[0]["salt"].(string) + if !ok { + salt = "_bt_all_in_ssl" + } + passwd := form.Password + salt + // fmt.Println(passwd) + keyMd5 := md5.Sum([]byte(passwd)) + passwdMd5 := hex.EncodeToString(keyMd5[:]) + // fmt.Println(passwdMd5) + + if res[0]["password"] != passwdMd5 { + session.Set("__loginErrCount", ErrCount+1) + session.Set("__loginErrEnd", now) + _ = session.Save() + // c.JSON(http.StatusBadRequest, public.ResERR("密码错误")) + // 设置cookie + c.SetCookie("must_code", "1", 0, "/", "", false, false) + public.FailMsg(c, "密码错误") + return + } + + // session := sessions.Default(c) + session.Set("__loginErrCount", 0) + session.Delete("__loginErrEnd") + session.Set("login", true) + session.Set("__login_key", public.GetSettingIgnoreError("login_key")) + _ = session.Save() + // c.JSON(http.StatusOK, public.ResOK(0, nil, "登录成功")) + // 设置cookie + c.SetCookie("must_code", "1", -1, "/", "", false, true) + public.SuccessMsg(c, "登录成功") + return +} + +func GetCode(c *gin.Context) { + _, bs64, code, _ := public.GenerateCode() + session := sessions.Default(c) + + session.Set("_verifyCode", code) + _ = session.Save() + public.SuccessData(c, bs64, 0) + return +} + +func SignOut(c *gin.Context) { + session := sessions.Default(c) + session.Delete("login") + _ = session.Save() + // c.JSON(http.StatusOK, public.ResOK(0, nil, "登出成功")) + public.SuccessMsg(c, "登出成功") + return +} diff --git a/backend/app/api/overview.go b/backend/app/api/overview.go new file mode 100644 index 0000000..6aa23d2 --- /dev/null +++ b/backend/app/api/overview.go @@ -0,0 +1,20 @@ +package api + +import ( + "ALLinSSL/backend/internal/overview" + "ALLinSSL/backend/public" + "github.com/gin-gonic/gin" +) + +func GetOverview(c *gin.Context) { + // Get the overview data from the database + overviewData, err := overview.GetOverviewData() + if err != nil { + public.FailMsg(c, err.Error()) + return + } + + // Return the overview data as JSON + public.SuccessData(c, overviewData, 0) + +} diff --git a/backend/app/api/report.go b/backend/app/api/report.go new file mode 100644 index 0000000..410a569 --- /dev/null +++ b/backend/app/api/report.go @@ -0,0 +1,102 @@ +package api + +import ( + "ALLinSSL/backend/internal/report" + "ALLinSSL/backend/public" + "github.com/gin-gonic/gin" +) + +func GetReportList(c *gin.Context) { + var form struct { + Search string `form:"search"` + Page int64 `form:"p"` + Limit int64 `form:"limit"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + certList, count, err := report.GetList(form.Search, form.Page, form.Limit) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessData(c, certList, count) + return +} + +func AddReport(c *gin.Context) { + var form struct { + Name string `form:"name"` + Type string `form:"type"` + Config string `form:"config"` + } + err := c.Bind(&form) + if err != nil { + // fmt.Println(err) + public.FailMsg(c, err.Error()) + return + } + err = report.AddReport(form.Type, form.Config, form.Name) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "添加成功") + return +} + +func UpdReport(c *gin.Context) { + var form struct { + Id string `form:"id"` + Name string `form:"name"` + Config string `form:"config"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + err = report.UpdReport(form.Id, form.Config, form.Name) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "修改成功") + return +} + +func DelReport(c *gin.Context) { + var form struct { + Id string `form:"id"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + err = report.DelReport(form.Id) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "删除成功") +} + +func NotifyTest(c *gin.Context) { + var form struct { + Id string `form:"id"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + err = report.NotifyTest(form.Id) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "发送成功") +} diff --git a/backend/app/api/setting.go b/backend/app/api/setting.go new file mode 100644 index 0000000..e63e8ce --- /dev/null +++ b/backend/app/api/setting.go @@ -0,0 +1,40 @@ +package api + +import ( + "ALLinSSL/backend/internal/setting" + "ALLinSSL/backend/public" + "github.com/gin-gonic/gin" +) + +func GetSetting(c *gin.Context) { + data, err := setting.Get() + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessData(c, data, 0) +} + +func SaveSetting(c *gin.Context) { + var data setting.Setting + if err := c.Bind(&data); err != nil { + public.FailMsg(c, "参数错误") + return + } + if err := setting.Save(&data); err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "保存成功") + +} + +func Shutdown(c *gin.Context) { + setting.Shutdown() + public.SuccessMsg(c, "关闭成功") +} + +func Restart(c *gin.Context) { + setting.Restart() + public.SuccessMsg(c, "正在重启...") +} diff --git a/backend/app/api/siteMonitor.go b/backend/app/api/siteMonitor.go new file mode 100644 index 0000000..ea1b1a3 --- /dev/null +++ b/backend/app/api/siteMonitor.go @@ -0,0 +1,131 @@ +package api + +import ( + "ALLinSSL/backend/internal/siteMonitor" + "ALLinSSL/backend/public" + "github.com/gin-gonic/gin" + "strings" +) + +func GetMonitorList(c *gin.Context) { + var form struct { + Search string `form:"search"` + Page int64 `form:"p"` + Limit int64 `form:"limit"` + } + err := c.Bind(&form) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + data, count, err := siteMonitor.GetList(form.Search, form.Page, form.Limit) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + // c.JSON(http.StatusOK, public.ResOK(len(data), data, "")) + public.SuccessData(c, data, count) + return +} + +func AddMonitor(c *gin.Context) { + var form struct { + Name string `form:"name"` + Domain string `form:"domain"` + Cycle int `form:"cycle"` + ReportType string `form:"report_type"` + } + err := c.Bind(&form) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + form.Name = strings.TrimSpace(form.Name) + form.Domain = strings.TrimSpace(form.Domain) + + err = siteMonitor.AddMonitor(form.Name, form.Domain, form.ReportType, form.Cycle) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + // c.JSON(http.StatusOK, public.ResOK(0, nil, "添加成功")) + public.SuccessMsg(c, "添加成功") + return +} + +func UpdMonitor(c *gin.Context) { + var form struct { + ID string `form:"id"` + Name string `form:"name"` + Domain string `form:"domain"` + Cycle int `form:"cycle"` + ReportType string `form:"report_type"` + } + err := c.Bind(&form) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + form.ID = strings.TrimSpace(form.ID) + form.Name = strings.TrimSpace(form.Name) + form.Domain = strings.TrimSpace(form.Domain) + form.ReportType = strings.TrimSpace(form.ReportType) + + err = siteMonitor.UpdMonitor(form.ID, form.Name, form.Domain, form.ReportType, form.Cycle) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + // c.JSON(http.StatusOK, public.ResOK(0, nil, "修改成功")) + public.SuccessMsg(c, "修改成功") + return +} + +func DelMonitor(c *gin.Context) { + var form struct { + ID string `form:"id"` + } + err := c.Bind(&form) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + err = siteMonitor.DelMonitor(form.ID) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + // c.JSON(http.StatusOK, public.ResOK(0, nil, "删除成功")) + public.SuccessMsg(c, "删除成功") + return +} + +func SetMonitor(c *gin.Context) { + var form struct { + ID string `form:"id"` + Active int `form:"active"` + } + err := c.Bind(&form) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + err = siteMonitor.SetMonitor(form.ID, form.Active) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + // c.JSON(http.StatusOK, public.ResOK(0, nil, "操作成功")) + public.SuccessMsg(c, "操作成功") + return +} diff --git a/backend/app/api/workflow.go b/backend/app/api/workflow.go new file mode 100644 index 0000000..03ee978 --- /dev/null +++ b/backend/app/api/workflow.go @@ -0,0 +1,236 @@ +package api + +import ( + "ALLinSSL/backend/internal/workflow" + "ALLinSSL/backend/public" + "github.com/gin-gonic/gin" + "strings" +) + +func GetWorkflowList(c *gin.Context) { + var form struct { + Search string `form:"search"` + Page int64 `form:"p"` + Limit int64 `form:"limit"` + } + err := c.Bind(&form) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + + data, count, err := workflow.GetList(form.Search, form.Page, form.Limit) + if err != nil { + // c.JSON(http.StatusBadRequest, public.ResERR(err.Error())) + public.FailMsg(c, err.Error()) + return + } + // c.JSON(http.StatusOK, public.ResOK(len(data), data, "")) + public.SuccessData(c, data, count) + return +} + +func AddWorkflow(c *gin.Context) { + var form struct { + Name string `form:"name"` + Content string `form:"content"` + ExecType string `form:"exec_type"` + Active string `form:"active"` + ExecTime string `form:"exec_time"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.Name = strings.TrimSpace(form.Name) + form.ExecType = strings.TrimSpace(form.ExecType) + + err = workflow.AddWorkflow(form.Name, form.Content, form.ExecType, form.Active, form.ExecTime) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "添加成功") + return +} + +func DelWorkflow(c *gin.Context) { + var form struct { + ID string `form:"id"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.ID = strings.TrimSpace(form.ID) + + err = workflow.DelWorkflow(form.ID) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "删除成功") + return + +} + +func UpdWorkflow(c *gin.Context) { + var form struct { + ID string `form:"id"` + Name string `form:"name"` + Content string `form:"content"` + ExecType string `form:"exec_type"` + Active string `form:"active"` + ExecTime string `form:"exec_time"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.ID = strings.TrimSpace(form.ID) + form.Name = strings.TrimSpace(form.Name) + form.ExecType = strings.TrimSpace(form.ExecType) + + err = workflow.UpdWorkflow(form.ID, form.Name, form.Content, form.ExecType, form.Active, form.ExecTime) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "修改成功") + return +} + +func UpdExecType(c *gin.Context) { + var form struct { + ID string `form:"id"` + ExecType string `form:"exec_type"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.ID = strings.TrimSpace(form.ID) + form.ExecType = strings.TrimSpace(form.ExecType) + + err = workflow.UpdExecType(form.ID, form.ExecType) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "修改成功") + return +} + +func UpdActive(c *gin.Context) { + var form struct { + ID string `form:"id"` + Active string `form:"active"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + + form.ID = strings.TrimSpace(form.ID) + form.Active = strings.TrimSpace(form.Active) + if form.ID == "" { + public.FailMsg(c, "ID不能为空") + return + } + + err = workflow.UpdActive(form.ID, form.Active) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "修改成功") + return +} + +func ExecuteWorkflow(c *gin.Context) { + var form struct { + ID string `form:"id"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.ID = strings.TrimSpace(form.ID) + + err = workflow.ExecuteWorkflow(form.ID) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "执行成功") + return +} + +func StopWorkflow(c *gin.Context) { + var form struct { + ID string `form:"id"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.ID = strings.TrimSpace(form.ID) + + err = workflow.StopWorkflow(form.ID) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessMsg(c, "停止成功") + return +} + +func GetWorkflowHistory(c *gin.Context) { + var form struct { + ID string `form:"id"` + Page int64 `form:"p"` + Limit int64 `form:"limit"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.ID = strings.TrimSpace(form.ID) + + data, count, err := workflow.GetListWH(form.ID, form.Page, form.Limit) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessData(c, data, count) + return +} + +func GetExecLog(c *gin.Context) { + var form struct { + ID string `form:"id"` + } + err := c.Bind(&form) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + form.ID = strings.TrimSpace(form.ID) + + data, err := workflow.GetExecLog(form.ID) + if err != nil { + public.FailMsg(c, err.Error()) + return + } + public.SuccessData(c, data, 0) + return +} diff --git a/backend/internal/access/access.go b/backend/internal/access/access.go new file mode 100644 index 0000000..210e5a3 --- /dev/null +++ b/backend/internal/access/access.go @@ -0,0 +1,149 @@ +package access + +import ( + "ALLinSSL/backend/public" + "fmt" + "strings" + "time" +) + +func GetSqlite() (*public.Sqlite, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + s.TableName = "access" + return s, nil +} + +func GetList(search string, p, limit int64) ([]map[string]any, int, error) { + var data []map[string]any + var count int64 + s, err := GetSqlite() + if err != nil { + return data, 0, err + } + defer s.Close() + + var limits []int64 + if p >= 0 && limit >= 0 { + limits = []int64{0, limit} + if p > 1 { + limits[0] = (p - 1) * limit + limits[1] = p * limit + } + } + if search != "" { + count, err = s.Where("name like ? or type like ?", []interface{}{"%" + search + "%", "%" + search + "%"}).Count() + data, err = s.Where("name like ? or type like ?", []interface{}{"%" + search + "%", "%" + search + "%"}).Order("update_time", "desc").Limit(limits).Select() + } else { + count, err = s.Count() + data, err = s.Order("update_time", "desc").Limit(limits).Select() + } + + if err != nil { + return data, 0, err + } + ATMap := GetAccessTypeMap("name", "type") + for _, v := range data { + v["access_type"] = ATMap[v["type"].(string)] + } + + return data, int(count), nil +} + +func GetAll(Type string) ([]map[string]any, error) { + var data []map[string]any + s, err := GetSqlite() + if err != nil { + return data, err + } + defer s.Close() + + ATMap := GetAccessTypeMap("type", "name") + + if Type != "" { + if Type == "dns" { + TypeL := strings.Join(ATMap["dns"], "','") + data, err = s.Where(fmt.Sprintf("type in ('%s')", TypeL), []interface{}{}).Select() + } else { + Type := strings.Split(strings.TrimPrefix(Type, "-"), "-")[0] + data, err = s.Where("type = ?", []interface{}{Type}).Select() + } + } else { + data, err = s.Select() + } + if err != nil { + return data, err + } + return data, nil +} + +func GetAccess(ID string) (map[string]any, error) { + s, err := GetSqlite() + if err != nil { + return nil, err + } + defer s.Close() + data, err := s.Where("id = ?", []interface{}{ID}).Select() + if err != nil { + return nil, err + } + if len(data) == 0 { + return nil, fmt.Errorf("API授权不存在:%s", ID) + } + return data[0], nil +} + +func AddAccess(config, name, typ string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + now := time.Now().Format("2006-01-02 15:04:05") + _, err = s.Insert(map[string]any{ + "name": name, + "type": typ, + "config": config, + "create_time": now, + "update_time": now, + }) + if err != nil { + return err + } + return nil +} + +func UpdateAccess(id, config, name string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + now := time.Now().Format("2006-01-02 15:04:05") + _, err = s.Where("id = ?", []interface{}{id}).Update(map[string]any{ + "name": name, + "config": config, + "update_time": now, + }) + if err != nil { + return err + } + return nil +} + +func DelAccess(id string) error { + s, err := GetSqlite() + + if err != nil { + return err + } + defer s.Close() + _, err = s.Where("id = ?", []interface{}{id}).Delete() + if err != nil { + return err + } + return nil +} diff --git a/backend/internal/access/accessType.go b/backend/internal/access/accessType.go new file mode 100644 index 0000000..7257681 --- /dev/null +++ b/backend/internal/access/accessType.go @@ -0,0 +1,36 @@ +package access + +import ( + "ALLinSSL/backend/public" +) + +func GetSqliteAT() (*public.Sqlite, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + s.TableName = "access_type" + return s, nil +} + +func GetAccessTypeMap(key, val string) map[string][]string { + dataMap := make(map[string][]string) + s, err := GetSqliteAT() + if err != nil { + return dataMap + } + defer s.Close() + data, err := s.Select() + if err != nil { + return dataMap + } + for _, row := range data { + if dataMap[row[key].(string)] == nil { + dataMap[row[key].(string)] = []string{row[val].(string)} + } else { + dataMap[row[key].(string)] = append(dataMap[row[key].(string)], row[val].(string)) + } + } + return dataMap +} diff --git a/backend/internal/cert/apply/account.go b/backend/internal/cert/apply/account.go new file mode 100644 index 0000000..af91cbb --- /dev/null +++ b/backend/internal/cert/apply/account.go @@ -0,0 +1,94 @@ +package apply + +import ( + "ALLinSSL/backend/public" + "crypto" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "github.com/go-acme/lego/v4/registration" + "time" +) + +type MyUser struct { + Email string + Registration *registration.Resource + key crypto.PrivateKey +} + +func (u *MyUser) GetEmail() string { + return u.Email +} + +func (u *MyUser) GetRegistration() *registration.Resource { + return u.Registration +} + +func (u *MyUser) GetPrivateKey() crypto.PrivateKey { + return u.key +} + +func SaveUserToDB(db *public.Sqlite, user *MyUser) error { + keyBytes, err := x509.MarshalPKCS8PrivateKey(user.key) + if err != nil { + return err + } + regBytes := []byte("") + if user.Registration != nil { + regBytes, err = json.Marshal(user.Registration) + if err != nil { + return err + } + } + + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keyBytes, + }) + now := time.Now().Format("2006-01-02 15:04:05") + _, err = db.Insert(map[string]interface{}{ + "email": user.Email, + "private_key": string(pemBytes), + "reg": regBytes, + "create_time": now, + "update_time": now, + "type": "Let's Encrypt", + }) + return err +} + +func LoadUserFromDB(db *public.Sqlite, email string) (*MyUser, error) { + data, err := db.Where(`email=?`, []interface{}{email}).Select() + if err != nil { + return nil, err + } + if len(data) == 0 { + return nil, fmt.Errorf("user not found") + } + regStr, ok := data[0]["reg"].(string) + if !ok { + return nil, fmt.Errorf("invalid reg data") + } + regBytes := []byte(regStr) + privPEM, ok := data[0]["private_key"].(string) + if !ok { + return nil, fmt.Errorf("invalid private key data") + } + privateKey, err := public.ParsePrivateKey([]byte(privPEM)) + if err != nil { + return nil, err + } + var reg *registration.Resource + if len(regBytes) > 0 { + reg = ®istration.Resource{} + if err := json.Unmarshal(regBytes, reg); err != nil { + return nil, err + } + } + return &MyUser{ + Email: email, + key: privateKey, + Registration: reg, + }, nil +} diff --git a/backend/internal/cert/apply/apply.go b/backend/internal/cert/apply/apply.go new file mode 100644 index 0000000..d4c22d4 --- /dev/null +++ b/backend/internal/cert/apply/apply.go @@ -0,0 +1,245 @@ +package apply + +import ( + "ALLinSSL/backend/internal/access" + "ALLinSSL/backend/internal/cert" + "ALLinSSL/backend/public" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "fmt" + "github.com/go-acme/lego/v4/certcrypto" + "github.com/go-acme/lego/v4/certificate" + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/lego" + "github.com/go-acme/lego/v4/providers/dns/alidns" + "github.com/go-acme/lego/v4/providers/dns/tencentcloud" + "github.com/go-acme/lego/v4/registration" + "strconv" + "strings" + "time" +) + +func GetSqlite() (*public.Sqlite, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + s.TableName = "_accounts" + return s, nil +} + +func GetDNSProvider(providerName string, creds map[string]string) (challenge.Provider, error) { + switch providerName { + case "tencentcloud": + config := tencentcloud.NewDefaultConfig() + config.SecretID = creds["secret_id"] + config.SecretKey = creds["secret_key"] + return tencentcloud.NewDNSProviderConfig(config) + + // case "cloudflare": + // config := cloudflare.NewDefaultConfig() + // config.AuthToken = creds["CLOUDFLARE_API_TOKEN"] + // return cloudflare.NewDNSProviderConfig(config) + + case "aliyun": + config := alidns.NewDefaultConfig() + config.APIKey = creds["access_key"] + config.SecretKey = creds["access_secret"] + return alidns.NewDNSProviderConfig(config) + + default: + return nil, fmt.Errorf("不支持的 DNS Provider: %s", providerName) + } +} + +func Apply(cfg map[string]any, logger *public.Logger) (map[string]any, error) { + db, err := GetSqlite() + if err != nil { + return nil, err + } + defer db.Close() + + email, ok := cfg["email"].(string) + if !ok { + return nil, fmt.Errorf("参数错误:email") + } + domains, ok := cfg["domains"].(string) + if !ok { + return nil, fmt.Errorf("参数错误:domains") + } + providerStr, ok := cfg["provider"].(string) + if !ok { + return nil, fmt.Errorf("参数错误:provider") + } + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return nil, fmt.Errorf("参数错误:provider_id") + } + + // 获取上次申请的证书 + runId, ok := cfg["_runId"].(string) + if !ok { + return nil, fmt.Errorf("参数错误:_runId") + } + if runId != "" { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + s.TableName = "workflow_history" + defer s.Close() + // 查询 workflowId + wh, err := s.Where("id=?", []interface{}{runId}).Select() + if err != nil { + return nil, err + } + if len(wh) > 0 { + s.TableName = "cert" + certs, err := s.Where("workflow_id=?", []interface{}{wh[0]["workflow_id"]}).Select() + if err != nil { + return nil, err + } + if len(certs) > 0 { + layout := "2006-01-02 15:04:05" + var maxDays float64 + var maxItem map[string]any + for i := range certs { + endTimeStr, ok := certs[i]["end_time"].(string) + if !ok { + continue + } + endTime, _ := time.Parse(layout, endTimeStr) + diff := endTime.Sub(time.Now()).Hours() / 24 + if diff > maxDays { + maxDays = diff + maxItem = certs[i] + } + } + certObj := maxItem + // 判断证书是否过期 + cfgEnd, ok := cfg["end_day"].(int) + if !ok || cfgEnd <= 0 { + cfgEnd = 30 + } + + if int(maxDays) > cfgEnd { + // 证书未过期,直接返回 + logger.Debug(fmt.Sprintf("上次证书申请成功,剩余天数:%d 大于%d天,已跳过申请复用此证书", int(maxDays), cfgEnd)) + return map[string]any{ + "cert": certObj["cert"], + "key": certObj["key"], + "issuerCert": certObj["issuer_cert"], + }, nil + } + } + } + } + logger.Debug("正在申请证书,域名: " + domains) + + user, err := LoadUserFromDB(db, email) + if err != nil { + logger.Debug("acme账号不存在,注册新账号") + privateKey, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + user = &MyUser{ + Email: email, + key: privateKey, + } + + config := lego.NewConfig(user) + config.Certificate.KeyType = certcrypto.EC384 + + client, err := lego.NewClient(config) + if err != nil { + return nil, err + } + logger.Debug("正在注册账号:" + email) + reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if err != nil { + return nil, err + } + user.Registration = reg + + err = SaveUserToDB(db, user) + if err != nil { + return nil, err + } + logger.Debug("账号注册并保存成功") + } + + // 初始化 ACME 客户端 + client, err := lego.NewClient(lego.NewConfig(user)) + if err != nil { + return nil, err + } + // 获取 DNS 验证提供者 + providerData, err := access.GetAccess(providerID) + if err != nil { + return nil, err + } + providerConfigStr, ok := providerData["config"].(string) + if !ok { + return nil, fmt.Errorf("api配置错误") + } + // 解析 JSON 配置 + var providerConfig map[string]string + err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) + if err != nil { + return nil, err + } + + // DNS 验证 + provider, err := GetDNSProvider(providerStr, providerConfig) + if err != nil { + return nil, fmt.Errorf("创建 DNS provider 失败: %v", err) + } + + err = client.Challenge.SetDNS01Provider(provider, + dns01.WrapPreCheck(func(domain, fqdn, value string, check dns01.PreCheckFunc) (bool, error) { + // 跳过预检查 + return true, nil + }), + dns01.AddRecursiveNameservers([]string{ + "8.8.8.8:53", + "1.1.1.1:53", + })) + if err != nil { + return nil, err + } + + // fmt.Println(strings.Split(domains, ",")) + request := certificate.ObtainRequest{ + Domains: strings.Split(domains, ","), + Bundle: true, + } + certObj, err := client.Certificate.Obtain(request) + if err != nil { + return nil, err + } + + certStr := string(certObj.Certificate) + keyStr := string(certObj.PrivateKey) + issuerCertStr := string(certObj.IssuerCertificate) + + // 保存证书和私钥 + data := map[string]any{ + "cert": certStr, + "key": keyStr, + "issuerCert": issuerCertStr, + } + + err = cert.SaveCert("workflow", keyStr, certStr, issuerCertStr, runId) + if err != nil { + return nil, err + } + return data, nil +} diff --git a/backend/internal/cert/cert.go b/backend/internal/cert/cert.go new file mode 100644 index 0000000..979bf3a --- /dev/null +++ b/backend/internal/cert/cert.go @@ -0,0 +1,206 @@ +package cert + +import ( + "ALLinSSL/backend/public" + "fmt" + "strconv" + "strings" + "time" +) + +func GetSqlite() (*public.Sqlite, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + s.TableName = "cert" + return s, nil +} + +func GetList(search string, p, limit int64) ([]map[string]any, int, error) { + var data []map[string]any + var count int64 + s, err := GetSqlite() + if err != nil { + return data, 0, err + } + defer s.Close() + + var limits []int64 + if p >= 0 && limit >= 0 { + limits = []int64{0, limit} + if p > 1 { + limits[0] = (p - 1) * limit + limits[1] = p * limit + } + } + + if search != "" { + count, err = s.Where("domains like ?", []interface{}{"%" + search + "%"}).Count() + data, err = s.Where("domains like ?", []interface{}{"%" + search + "%"}).Limit(limits).Order("create_time", "desc").Select() + } else { + count, err = s.Count() + data, err = s.Order("create_time", "desc").Limit(limits).Select() + } + if err != nil { + return data, 0, err + } + for _, v := range data { + endtime, err := time.Parse("2006-01-02 15:04:05", v["end_time"].(string)) + if err != nil { + continue + } + v["end_day"] = strconv.FormatInt(int64(endtime.Sub(time.Now())/(24*time.Hour)), 10) + } + return data, int(count), nil +} + +func AddCert(source, key, cert, issuer, issuerCert, domains, sha256, historyId, startTime, endTime, endDay string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + workflowId := "" + if historyId != "" { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return err + } + s.Connect() + s.TableName = "workflow_history" + defer s.Close() + // 查询 workflowId + wh, err := s.Where("id=?", []interface{}{historyId}).Select() + if err != nil { + return err + } + if len(wh) > 0 { + workflowId = wh[0]["workflow_id"].(string) + } + } + + now := time.Now().Format("2006-01-02 15:04:05") + _, err = s.Insert(map[string]any{ + "source": source, + "key": key, + "cert": cert, + "issuer": issuer, + "issuer_cert": issuerCert, + "domains": domains, + "sha256": sha256, + "history_id": historyId, + "workflow_id": workflowId, + "create_time": now, + "update_time": now, + "start_time": startTime, + "end_time": endTime, + "end_day": endDay, + }) + if err != nil { + return err + } + return nil +} + +func SaveCert(source, key, cert, issuerCert, historyId string) error { + if err := public.ValidateSSLCertificate(cert, key); err != nil { + return err + } + + certObj, err := public.ParseCertificate([]byte(cert)) + if err != nil { + return fmt.Errorf("解析证书失败: %v", err) + } + // SHA256 + sha256, err := public.GetSHA256(cert) + if err != nil { + return fmt.Errorf("获取 SHA256 失败: %v", err) + } + if d, _ := GetCert(sha256); d != nil { + return nil + } + + domainSet := make(map[string]bool) + + if certObj.Subject.CommonName != "" { + domainSet[certObj.Subject.CommonName] = true + } + for _, dns := range certObj.DNSNames { + domainSet[dns] = true + } + + // 转成切片并拼接成逗号分隔的字符串 + var domains []string + for domain := range domainSet { + domains = append(domains, domain) + } + domainList := strings.Join(domains, ",") + + // 提取 CA 名称(Issuer 的组织名) + caName := "UNKNOWN" + if len(certObj.Issuer.Organization) > 0 { + caName = certObj.Issuer.Organization[0] + } else if certObj.Issuer.CommonName != "" { + caName = certObj.Issuer.CommonName + } + // 证书有效期 + startTime := certObj.NotBefore.Format("2006-01-02 15:04:05") + endTime := certObj.NotAfter.Format("2006-01-02 15:04:05") + endDay := fmt.Sprintf("%d", int(certObj.NotAfter.Sub(time.Now()).Hours()/24)) + + err = AddCert(source, key, cert, caName, issuerCert, domainList, sha256, historyId, startTime, endTime, endDay) + if err != nil { + return fmt.Errorf("保存证书失败: %v", err) + } + return nil +} + +func UploadCert(key, cert string) error { + err := SaveCert("upload", key, cert, "", "") + if err != nil { + return fmt.Errorf("保存证书失败: %v", err) + } + return nil +} + +func DelCert(id string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + + _, err = s.Where("id=?", []interface{}{id}).Delete() + if err != nil { + return err + } + return nil +} + +func GetCert(id string) (map[string]string, error) { + s, err := GetSqlite() + if err != nil { + return nil, err + } + defer s.Close() + + res, err := s.Where("id=? or sha256=?", []interface{}{id, id}).Select() + if err != nil { + return nil, err + } + if len(res) == 0 { + return nil, fmt.Errorf("证书不存在") + } + + data := map[string]string{ + "domains": res[0]["domains"].(string), + "cert": res[0]["cert"].(string), + "key": res[0]["key"].(string), + } + + return data, nil +} + +// ======================================================== diff --git a/backend/internal/cert/deploy/1p_test.go b/backend/internal/cert/deploy/1p_test.go new file mode 100644 index 0000000..93e4269 --- /dev/null +++ b/backend/internal/cert/deploy/1p_test.go @@ -0,0 +1,31 @@ +package deploy + +import "testing" + +func TestSite(t *testing.T) { + cfg := map[string]any{ + "site_id": "1", + "provider_id": "22", + "certificate": map[string]any{ + "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxIjmAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONh\nWhMT6W+cx0WMC80yCRm5JshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM\n4tz/Zh8a3kVyN4MtWDmV1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1\nz6qqEDcM8FtHoAXAdxQBkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9\ncMY1cCEBxpwQTJiJHbX9LcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MU\nS89+DsixFf3HL+iWjr6yVnQ/mAGVPQ+HD4pwmQIDAQABAoIBAALpcFb59MBZZHJ3\nui9RRi96ig6kPQoRjkjN83pjM+/h/bANMmUOQU5FHBKLwj5uhN5Dpk2fzAnIX2TE\nVgfyNGsYuWLsIM+m6EJfm7pXJwJDr3RCpm+6DIKr1U8TwlR2OhbDi6fOlfH66q79\n2Klq4SXsa0vgfllpTVCDtydFVjwAuQV7Cf6DGRjbNpN3DPLeOC1wYFimNZwudSK0\nf8grWpPFXw2TPaf3TgeBGxwL7GCTYSKT+Eq9USbhG4RArrM9oQt+h7rzaH2bFEdg\n7tOM4KIgV+aw8r0TsYisDG9dfiHfHr5vQnkmWgt/rxAOvHlJ7/64pBVuET1ZF0mB\nP6gu4Y0CgYEAzkwXvfnHI5qx9BVP6e9lGrpWrm0RxCKr2iCCwrOVALbX1yfKCb5L\nrP/jSERMuLt6bIKg/AoVu9ogCTGzntyHTbZXFGg/y5Xoul+1af2arQ1rGZ7A/Im7\nnteZePg2U6UiDRy07F94FF5aL/v97D4BffiSA+0atlgH6tpKyYfY6NsCgYEA8+Ku\nGQqX9kHDd5bbzPhLelNmHVnAjnMaHEhvzVtBA737F10Oqg9wyffqe/i/DvdUSx9r\nafKGUfzB2vVZjz//OpSQ8VhRzDTiyelKLsSTmzOokLBnwayyTxw85o9EDvTNrzfb\nYQbAjmAXWmnv5Xvx1KfvTaKFY3BmHsKYJDzwnJsCgYBK1SVjn2CSVMIqlTSI2nMl\nb+STnzLrn9wQ4uwr7nKlcK34+RD72dCfr67lfwkJldBB3lzBMHNT0jr+us26Waqn\nEPaji3Fgyz9BpAgtq3XZQl3QTFsbAGdTpkegrwEd9G/Wq8whVjw7v0Id193zPUbT\nSEDHNdITxPkSQx8P3bxcMwKBgQDO5EGk5KO9OFTFoqib3RbKku1RgM4lCefgjmKp\n5vvkXMohK8RA6BBahYHZ4U7TN2W+xMyueBsSekVJplFvgG7YFyhOVQovHb42Yz2X\nJxPA2bXp6HxchFBPZDkVrfuiZHIIbm4ghUXcgg/Nl4j3OIoSSNRtG63kiXlYJuRB\n+aB0eQKBgD79VrREpbOMS7HRlDTtfkDN94HY3T4MLErs26z/NLO/dC44tmBJGo2P\ngcQ+p7XxNjpWUnUbEiuz4R3Xgh6ULwuSseWtcQicolPHTkBjnc+6BEpyguZJ+FPZ\nGls3g3LxjGhdPlyd37CaWDvx/Jtjrd4Y9iGkGO2d9fXZD0Hg0ymX\n-----END RSA PRIVATE KEY-----", + "cert": "-----BEGIN CERTIFICATE-----\nMIIG5DCCBMygAwIBAgIQBPQGlt81+4RKt3RAFXPvrjANBgkqhkiG9w0BAQsFADBb\nMQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywg\nSW5jLjElMCMGA1UEAxMcVHJ1c3RBc2lhIERWIFRMUyBSU0EgQ0EgMjAyNTAeFw0y\nNTA0MjIwMDAwMDBaFw0yNTA3MjAyMzU5NTlaMB8xHTAbBgNVBAMTFGFsbGluc3Ns\nLnphY2h5YW5nLmNuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxIjm\nAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONhWhMT6W+cx0WMC80yCRm5\nJshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM4tz/Zh8a3kVyN4MtWDmV\n1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1z6qqEDcM8FtHoAXAdxQB\nkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9cMY1cCEBxpwQTJiJHbX9\nLcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MUS89+DsixFf3HL+iWjr6y\nVnQ/mAGVPQ+HD4pwmQIDAQABo4IC3jCCAtowHwYDVR0jBBgwFoAUtBIopbTAHZ8p\ncWk82RGWSnVpUMAwHQYDVR0OBBYEFHqqdlMVBlcadf7iJLJoLnLZ7h4tMB8GA1Ud\nEQQYMBaCFGFsbGluc3NsLnphY2h5YW5nLmNuMD4GA1UdIAQ3MDUwMwYGZ4EMAQIB\nMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNV\nHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHkGCCsG\nAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t\nMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vVHJ1c3RB\nc2lhRFZUTFNSU0FDQTIwMjUuY3J0MAwGA1UdEwEB/wQCMAAwggF9BgorBgEEAdZ5\nAgQCBIIBbQSCAWkBZwB2ABLxTjS9U3JMhAYZw48/ehP457Vih4icbTAFhOvlhiY6\nAAABll0w/o0AAAQDAEcwRQIgd24jCPm+fbHq3grMIxtvQhzkv7dvYPM/BGjPEsy1\nQ70CIQC5jXADjBh+dH50T+atn3lktBEqQhedOl6cAaP/XXmk6gB2AO08S9boBsKk\nogBX28sk4jgB31Ev7cSGxXAPIN23Pj/gAAABll0w/rUAAAQDAEcwRQIgU2GDVEH1\ns5i/RC1RhqvJjn72PAZOlDtJyLdg29vC9HECIQCj78GATYK5quitLxbn3HvD8BeT\noOz+3tacgyN6+TdvugB1AKRCxQZJYGFUjw/U6pz7ei0mRU2HqX8v30VZ9idPOoRU\nAAABll0w/sYAAAQDAEYwRAIgCvU/iBRPKoJLjmU4edBYObWAO/aJp2mWnfJ4ieAr\nrXsCIBsAppYu28h8YEOl0N9yEeF9G05IMxwkCjZKonQs2SKMMA0GCSqGSIb3DQEB\nCwUAA4ICAQB3wFou51Qvl4apMhencuQUnWF3UpYP49e0WQ72DVT3pYjYsozkSuqb\nQZcwMB6HDoHdFicxvQ/yxKyTu/nw3rXjUWYuSxXYd7lJcQ/R0tR00m6AFeinY4Aq\nq4QqoA+lriK1XqO5MomAL4FbSysT1ow/gaG9pYuXEdT4pr05I/NumjXdkwBRZOd4\nrhol2grKf3y37Qla5hUbbG3ab9nf/csJSWkCoESeXr3MB1oAU/aL9pGSagvMXSKQ\nsFs2cn2Fi8ZmJPJXIP114lgvFuFDO+C1yTNbHap/FufvAKGryfPDuPecCF6FSXej\n+bwg4/BNz5lcHbNo2XXjLgoPg4VE6mG/SQQZQEDBk5DowwMVMvh77t9RBNrHozah\nHGtQz2hCuIX7rZQYnSlvW8T75FhI/Sd+HEfU/iyTIELXBUjypnK2bOJL7+jE7f79\nuljhXlCcP52fGHCjexNBz5gIZr82KVxsfxKuZjfioPkhmWleVNMdMWYJRXu618E6\nNtNjUVsDCuMOOMNs1qScqxOT60MeDZLX+vnC93fdd/t2hLEAWWNNMkWeX2qLCE1q\nGarop9U1mJpiBWkW5cBiqnNIbhuV2fcwFIR8mVT5f1Qcw+WxE2nEjY2h75bKv8T5\n3RBngmaX8PcyLAP2s0/4UyzAnMYfioJBh37VpUYBrdriBkRds/AMZw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFnjCCBIagAwIBAgIQCSYyO0lk42hGFRLe8aXVLDANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0yNTAxMDgwMDAwMDBaFw0zNTAxMDcyMzU5NTlaMFsxCzAJBgNVBAYTAkNO\nMSUwIwYDVQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSUwIwYDVQQD\nExxUcnVzdEFzaWEgRFYgVExTIFJTQSBDQSAyMDI1MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA0fuEmuBIsN6ZZVq+gRobMorOGIilTCIfQrxNpR8FUZ9R\n/GfbiekbiIKphQXEZ7N1uBnn6tXUuZ32zl6jPkZpHzN/Bmgk1BWSIzVc0npMzrWq\n/hrbk5+KddXJdsNpeG1+Q8lc8uVMBrztnxaPb7Rh7yQCsMrcO4hgVaqLJWkVvEfW\nULtoCHQnNaj4IroG6VxQf1oArQ8bPbwpI02lieSahRa78FQuXdoGVeQcrkhtVjZs\nON98vq5fPWZX2LFv7e5J6P9IHbzvOl8yyQjv+2/IOwhNSkaXX3bI+//bqF9XW/p7\n+gsUmHiK5YsvLjmXcvDmoDEGrXMzgX31Zl2nJ+umpRbLjwP8rxYIUsKoEwEdFoto\nAid59UEBJyw/GibwXQ5xTyKD/N6C8SFkr1+myOo4oe1UB+YgvRu6qSxIABo5kYdX\nFodLP4IgoVJdeUFs1Usa6bxYEO6EgMf5lCWt9hGZszvXYZwvyZGq3ogNXM7eKyi2\n20WzJXYMmi9TYFq2Fa95aZe4wki6YhDhhOO1g0sjITGVaB73G+JOCI9yJhv6+REN\nD40ZpboUHE8JNgMVWbG1isAMVCXqiADgXtuC+tmJWPEH9cR6OuJLEpwOzPfgAbnn\n2MRu7Tsdr8jPjTPbD0FxblX1ydW3RG30vwLF5lkTTRkHG9epMgpPMdYP7nY/08MC\nAwEAAaOCAVYwggFSMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLQSKKW0\nwB2fKXFpPNkRlkp1aVDAMB8GA1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485\nMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\ndgYIKwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy\ndC5jb20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E\naWdpQ2VydEdsb2JhbFJvb3RHMi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov\nL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDARBgNV\nHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQELBQADggEBAJ4a3svh316GY2+Z7EYx\nmBIsOwjJSnyoEfzx2T699ctLLrvuzS79Mg3pPjxSLlUgyM8UzrFc5tgVU3dZ1sFQ\nI4RM+ysJdvIAX/7Yx1QbooVdKhkdi9X7QN7yVkjqwM3fY3WfQkRTzhIkM7mYIQbR\nr+y2Vkju61BLqh7OCRpPMiudjEpP1kEtRyGs2g0aQpEIqKBzxgitCXSayO1hoO6/\n71ts801OzYlqYW9OQQQ2GCJyFbD6XHDjdpn+bWUxTKWaMY0qedSCbHE3Kl2QEF0C\nynZ7SbC03yR+gKZQDeTXrNP1kk5Qhe7jSXgw+nhbspe0q/M1ZcNCz+sPxeOwdCcC\ngJE=\n-----END CERTIFICATE-----", + "issuer": "cert-issuer", + }, + } + err := Deploy1panelSite(cfg) + println(err) +} + +func TestP(t *testing.T) { + cfg := map[string]any{ + "site_id": "1", + "provider_id": "22", + "certificate": map[string]any{ + "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxIjmAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONh\nWhMT6W+cx0WMC80yCRm5JshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM\n4tz/Zh8a3kVyN4MtWDmV1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1\nz6qqEDcM8FtHoAXAdxQBkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9\ncMY1cCEBxpwQTJiJHbX9LcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MU\nS89+DsixFf3HL+iWjr6yVnQ/mAGVPQ+HD4pwmQIDAQABAoIBAALpcFb59MBZZHJ3\nui9RRi96ig6kPQoRjkjN83pjM+/h/bANMmUOQU5FHBKLwj5uhN5Dpk2fzAnIX2TE\nVgfyNGsYuWLsIM+m6EJfm7pXJwJDr3RCpm+6DIKr1U8TwlR2OhbDi6fOlfH66q79\n2Klq4SXsa0vgfllpTVCDtydFVjwAuQV7Cf6DGRjbNpN3DPLeOC1wYFimNZwudSK0\nf8grWpPFXw2TPaf3TgeBGxwL7GCTYSKT+Eq9USbhG4RArrM9oQt+h7rzaH2bFEdg\n7tOM4KIgV+aw8r0TsYisDG9dfiHfHr5vQnkmWgt/rxAOvHlJ7/64pBVuET1ZF0mB\nP6gu4Y0CgYEAzkwXvfnHI5qx9BVP6e9lGrpWrm0RxCKr2iCCwrOVALbX1yfKCb5L\nrP/jSERMuLt6bIKg/AoVu9ogCTGzntyHTbZXFGg/y5Xoul+1af2arQ1rGZ7A/Im7\nnteZePg2U6UiDRy07F94FF5aL/v97D4BffiSA+0atlgH6tpKyYfY6NsCgYEA8+Ku\nGQqX9kHDd5bbzPhLelNmHVnAjnMaHEhvzVtBA737F10Oqg9wyffqe/i/DvdUSx9r\nafKGUfzB2vVZjz//OpSQ8VhRzDTiyelKLsSTmzOokLBnwayyTxw85o9EDvTNrzfb\nYQbAjmAXWmnv5Xvx1KfvTaKFY3BmHsKYJDzwnJsCgYBK1SVjn2CSVMIqlTSI2nMl\nb+STnzLrn9wQ4uwr7nKlcK34+RD72dCfr67lfwkJldBB3lzBMHNT0jr+us26Waqn\nEPaji3Fgyz9BpAgtq3XZQl3QTFsbAGdTpkegrwEd9G/Wq8whVjw7v0Id193zPUbT\nSEDHNdITxPkSQx8P3bxcMwKBgQDO5EGk5KO9OFTFoqib3RbKku1RgM4lCefgjmKp\n5vvkXMohK8RA6BBahYHZ4U7TN2W+xMyueBsSekVJplFvgG7YFyhOVQovHb42Yz2X\nJxPA2bXp6HxchFBPZDkVrfuiZHIIbm4ghUXcgg/Nl4j3OIoSSNRtG63kiXlYJuRB\n+aB0eQKBgD79VrREpbOMS7HRlDTtfkDN94HY3T4MLErs26z/NLO/dC44tmBJGo2P\ngcQ+p7XxNjpWUnUbEiuz4R3Xgh6ULwuSseWtcQicolPHTkBjnc+6BEpyguZJ+FPZ\nGls3g3LxjGhdPlyd37CaWDvx/Jtjrd4Y9iGkGO2d9fXZD0Hg0ymX\n-----END RSA PRIVATE KEY-----", + "cert": "-----BEGIN CERTIFICATE-----\nMIIG5DCCBMygAwIBAgIQBPQGlt81+4RKt3RAFXPvrjANBgkqhkiG9w0BAQsFADBb\nMQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywg\nSW5jLjElMCMGA1UEAxMcVHJ1c3RBc2lhIERWIFRMUyBSU0EgQ0EgMjAyNTAeFw0y\nNTA0MjIwMDAwMDBaFw0yNTA3MjAyMzU5NTlaMB8xHTAbBgNVBAMTFGFsbGluc3Ns\nLnphY2h5YW5nLmNuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxIjm\nAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONhWhMT6W+cx0WMC80yCRm5\nJshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM4tz/Zh8a3kVyN4MtWDmV\n1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1z6qqEDcM8FtHoAXAdxQB\nkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9cMY1cCEBxpwQTJiJHbX9\nLcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MUS89+DsixFf3HL+iWjr6y\nVnQ/mAGVPQ+HD4pwmQIDAQABo4IC3jCCAtowHwYDVR0jBBgwFoAUtBIopbTAHZ8p\ncWk82RGWSnVpUMAwHQYDVR0OBBYEFHqqdlMVBlcadf7iJLJoLnLZ7h4tMB8GA1Ud\nEQQYMBaCFGFsbGluc3NsLnphY2h5YW5nLmNuMD4GA1UdIAQ3MDUwMwYGZ4EMAQIB\nMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNV\nHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHkGCCsG\nAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t\nMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vVHJ1c3RB\nc2lhRFZUTFNSU0FDQTIwMjUuY3J0MAwGA1UdEwEB/wQCMAAwggF9BgorBgEEAdZ5\nAgQCBIIBbQSCAWkBZwB2ABLxTjS9U3JMhAYZw48/ehP457Vih4icbTAFhOvlhiY6\nAAABll0w/o0AAAQDAEcwRQIgd24jCPm+fbHq3grMIxtvQhzkv7dvYPM/BGjPEsy1\nQ70CIQC5jXADjBh+dH50T+atn3lktBEqQhedOl6cAaP/XXmk6gB2AO08S9boBsKk\nogBX28sk4jgB31Ev7cSGxXAPIN23Pj/gAAABll0w/rUAAAQDAEcwRQIgU2GDVEH1\ns5i/RC1RhqvJjn72PAZOlDtJyLdg29vC9HECIQCj78GATYK5quitLxbn3HvD8BeT\noOz+3tacgyN6+TdvugB1AKRCxQZJYGFUjw/U6pz7ei0mRU2HqX8v30VZ9idPOoRU\nAAABll0w/sYAAAQDAEYwRAIgCvU/iBRPKoJLjmU4edBYObWAO/aJp2mWnfJ4ieAr\nrXsCIBsAppYu28h8YEOl0N9yEeF9G05IMxwkCjZKonQs2SKMMA0GCSqGSIb3DQEB\nCwUAA4ICAQB3wFou51Qvl4apMhencuQUnWF3UpYP49e0WQ72DVT3pYjYsozkSuqb\nQZcwMB6HDoHdFicxvQ/yxKyTu/nw3rXjUWYuSxXYd7lJcQ/R0tR00m6AFeinY4Aq\nq4QqoA+lriK1XqO5MomAL4FbSysT1ow/gaG9pYuXEdT4pr05I/NumjXdkwBRZOd4\nrhol2grKf3y37Qla5hUbbG3ab9nf/csJSWkCoESeXr3MB1oAU/aL9pGSagvMXSKQ\nsFs2cn2Fi8ZmJPJXIP114lgvFuFDO+C1yTNbHap/FufvAKGryfPDuPecCF6FSXej\n+bwg4/BNz5lcHbNo2XXjLgoPg4VE6mG/SQQZQEDBk5DowwMVMvh77t9RBNrHozah\nHGtQz2hCuIX7rZQYnSlvW8T75FhI/Sd+HEfU/iyTIELXBUjypnK2bOJL7+jE7f79\nuljhXlCcP52fGHCjexNBz5gIZr82KVxsfxKuZjfioPkhmWleVNMdMWYJRXu618E6\nNtNjUVsDCuMOOMNs1qScqxOT60MeDZLX+vnC93fdd/t2hLEAWWNNMkWeX2qLCE1q\nGarop9U1mJpiBWkW5cBiqnNIbhuV2fcwFIR8mVT5f1Qcw+WxE2nEjY2h75bKv8T5\n3RBngmaX8PcyLAP2s0/4UyzAnMYfioJBh37VpUYBrdriBkRds/AMZw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFnjCCBIagAwIBAgIQCSYyO0lk42hGFRLe8aXVLDANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0yNTAxMDgwMDAwMDBaFw0zNTAxMDcyMzU5NTlaMFsxCzAJBgNVBAYTAkNO\nMSUwIwYDVQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSUwIwYDVQQD\nExxUcnVzdEFzaWEgRFYgVExTIFJTQSBDQSAyMDI1MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA0fuEmuBIsN6ZZVq+gRobMorOGIilTCIfQrxNpR8FUZ9R\n/GfbiekbiIKphQXEZ7N1uBnn6tXUuZ32zl6jPkZpHzN/Bmgk1BWSIzVc0npMzrWq\n/hrbk5+KddXJdsNpeG1+Q8lc8uVMBrztnxaPb7Rh7yQCsMrcO4hgVaqLJWkVvEfW\nULtoCHQnNaj4IroG6VxQf1oArQ8bPbwpI02lieSahRa78FQuXdoGVeQcrkhtVjZs\nON98vq5fPWZX2LFv7e5J6P9IHbzvOl8yyQjv+2/IOwhNSkaXX3bI+//bqF9XW/p7\n+gsUmHiK5YsvLjmXcvDmoDEGrXMzgX31Zl2nJ+umpRbLjwP8rxYIUsKoEwEdFoto\nAid59UEBJyw/GibwXQ5xTyKD/N6C8SFkr1+myOo4oe1UB+YgvRu6qSxIABo5kYdX\nFodLP4IgoVJdeUFs1Usa6bxYEO6EgMf5lCWt9hGZszvXYZwvyZGq3ogNXM7eKyi2\n20WzJXYMmi9TYFq2Fa95aZe4wki6YhDhhOO1g0sjITGVaB73G+JOCI9yJhv6+REN\nD40ZpboUHE8JNgMVWbG1isAMVCXqiADgXtuC+tmJWPEH9cR6OuJLEpwOzPfgAbnn\n2MRu7Tsdr8jPjTPbD0FxblX1ydW3RG30vwLF5lkTTRkHG9epMgpPMdYP7nY/08MC\nAwEAAaOCAVYwggFSMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLQSKKW0\nwB2fKXFpPNkRlkp1aVDAMB8GA1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485\nMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\ndgYIKwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy\ndC5jb20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E\naWdpQ2VydEdsb2JhbFJvb3RHMi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov\nL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDARBgNV\nHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQELBQADggEBAJ4a3svh316GY2+Z7EYx\nmBIsOwjJSnyoEfzx2T699ctLLrvuzS79Mg3pPjxSLlUgyM8UzrFc5tgVU3dZ1sFQ\nI4RM+ysJdvIAX/7Yx1QbooVdKhkdi9X7QN7yVkjqwM3fY3WfQkRTzhIkM7mYIQbR\nr+y2Vkju61BLqh7OCRpPMiudjEpP1kEtRyGs2g0aQpEIqKBzxgitCXSayO1hoO6/\n71ts801OzYlqYW9OQQQ2GCJyFbD6XHDjdpn+bWUxTKWaMY0qedSCbHE3Kl2QEF0C\nynZ7SbC03yR+gKZQDeTXrNP1kk5Qhe7jSXgw+nhbspe0q/M1ZcNCz+sPxeOwdCcC\ngJE=\n-----END CERTIFICATE-----", + "issuer": "cert-issuer", + }, + } + err := Deploy1panel(cfg) + println(err) +} diff --git a/backend/internal/cert/deploy/1panel.go b/backend/internal/cert/deploy/1panel.go new file mode 100644 index 0000000..51094dc --- /dev/null +++ b/backend/internal/cert/deploy/1panel.go @@ -0,0 +1,223 @@ +package deploy + +import ( + "ALLinSSL/backend/internal/access" + "bytes" + "crypto/md5" + "crypto/tls" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "time" +) + +func generateToken(timestamp string, apiKey string) string { + tokenMd5 := md5.Sum([]byte("1panel" + apiKey + timestamp)) + tokenMd5Hex := hex.EncodeToString(tokenMd5[:]) + return tokenMd5Hex +} + +// method provider_id url data + +func Request1panel(data *map[string]any, method, providerID, requestUrl string) (map[string]any, error) { + providerData, err := access.GetAccess(providerID) + if err != nil { + return nil, err + } + providerConfigStr, ok := providerData["config"].(string) + if !ok { + return nil, fmt.Errorf("api配置错误") + } + // 解析 JSON 配置 + var providerConfig map[string]string + err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) + if err != nil { + return nil, err + } + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + token := generateToken(timestamp, providerConfig["api_key"]) + + // data, requestUrl, method := GetDeploy1PBody(cfg, Type) + if requestUrl == "" || data == nil { + return nil, fmt.Errorf("不支持的部署类型") + } + + // 编码为 JSON + jsonData, err := json.Marshal(data) + if err != nil { + return nil, err + } + if providerConfig["url"][len(providerConfig["url"])-1:] != "/" { + providerConfig["url"] += "/" + } + + req, err := http.NewRequest(method, providerConfig["url"]+requestUrl, bytes.NewBuffer(jsonData)) + if err != nil { + // fmt.Println(err) + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36") + req.Header.Set("1Panel-Timestamp", timestamp) + req.Header.Set("1Panel-Token", token) + + // 自定义 Transport,跳过 SSL 证书验证 + ignoreSsl := false + if providerConfig["ignore_ssl"] == "1" { + ignoreSsl = true + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl}, + } + + client := &http.Client{Transport: tr} + resp, err := client.Do(req) + if err != nil { + // fmt.Println(err) + return nil, fmt.Errorf("请求1panel失败: %v", err) + } + body, _ := io.ReadAll(resp.Body) + defer resp.Body.Close() + + var res map[string]interface{} + err = json.Unmarshal(body, &res) + if err != nil { + return nil, fmt.Errorf("证书部署失败: %v", err) + } + code, ok := res["code"].(float64) + if !ok { + return nil, fmt.Errorf("证书部署失败") + } + if code != 200 { + msg, ok := res["msg"].(string) + if !ok { + return nil, fmt.Errorf("证书部署失败") + } + return nil, fmt.Errorf("证书部署失败: %s", msg) + } + return res, nil + +} + +func Deploy1panel(cfg map[string]any) error { + cert, ok := cfg["certificate"].(map[string]any) + if !ok { + return fmt.Errorf("证书不存在") + } + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return fmt.Errorf("参数错误:provider_id") + } + // 设置证书 + keyPem, ok := cert["key"].(string) + if !ok { + return fmt.Errorf("证书错误:key") + } + certPem, ok := cert["cert"].(string) + if !ok { + return fmt.Errorf("证书错误:cert") + } + + data := map[string]interface{}{ + "cert": certPem, + "key": keyPem, + "ssl": "enable", + "sslType": "import-paste", + } + _, err := Request1panel(&data, "POST", providerID, "api/v1/settings/ssl/update") + if err != nil { + return fmt.Errorf("证书部署失败: %v", err) + } + return nil +} + +func Deploy1panelSite(cfg map[string]any) error { + cert, ok := cfg["certificate"].(map[string]any) + if !ok { + return fmt.Errorf("证书不存在") + } + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return fmt.Errorf("参数错误:provider_id") + } + siteId, ok := cfg["site_id"].(string) + if !ok { + return fmt.Errorf("参数错误:site_id") + } + // 设置证书 + keyPem, ok := cert["key"].(string) + if !ok { + return fmt.Errorf("证书错误:key") + } + certPem, ok := cert["cert"].(string) + if !ok { + return fmt.Errorf("证书错误:cert") + } + // 获取网站参数 + siteData, err := Request1panel(&map[string]any{}, "GET", providerID, fmt.Sprintf("api/v1/websites/%s/https", siteId)) + if err != nil { + return fmt.Errorf("获取网站参数失败: %v", err) + } + // + websiteId, err := strconv.Atoi(siteId) + if err != nil { + return fmt.Errorf("获取网站参数失败: %v", err) + } + + siteData, ok = siteData["data"].(map[string]any) + if !ok { + return fmt.Errorf("获取网站参数失败: data") + } + SSLProtocol, ok := siteData["ssl_protocol"].(string) + if !ok { + return fmt.Errorf("获取网站参数失败: data.ssl_protocol") + } + algorithm, ok := siteData["algorithm"].(string) + if !ok { + return fmt.Errorf("获取网站参数失败: data.algorithm") + } + enable, ok := siteData["enable"].(bool) + if !ok { + return fmt.Errorf("获取网站参数失败: data.enable") + } + hsts, ok := siteData["hsts"].(bool) + if !ok { + return fmt.Errorf("获取网站参数失败: data.hsts") + } + httpConfig, ok := siteData["http_config"].(string) + if !ok { + return fmt.Errorf("获取网站参数失败: data.http_config") + } + + data := map[string]any{ + "SSLProtocol": SSLProtocol, + // "acmeAccountId": siteData["SSL"].(map[string]any)["acmeAccountId"].(float64), + "algorithm": algorithm, + "certificate": certPem, + "privateKey": keyPem, + // "certificatePath": "", + // "privateKeyPath": "", + "enable": enable, + "hsts": hsts, + "httpConfig": httpConfig, + // "importType": "paste", + "type": "manual", + "websiteId": websiteId, + } + _, err = Request1panel(&data, "POST", providerID, fmt.Sprintf("api/v1/websites/%s/https", siteId)) + return nil +} diff --git a/backend/internal/cert/deploy/ali_test.go b/backend/internal/cert/deploy/ali_test.go new file mode 100644 index 0000000..94b59c5 --- /dev/null +++ b/backend/internal/cert/deploy/ali_test.go @@ -0,0 +1,41 @@ +package deploy + +import "testing" + +func TestALiCdn(t *testing.T) { + cfg := map[string]any{ + "domain": "zwrnb.cn", + "provider_id": "24", + "certificate": map[string]any{ + "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxIjmAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONh\nWhMT6W+cx0WMC80yCRm5JshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM\n4tz/Zh8a3kVyN4MtWDmV1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1\nz6qqEDcM8FtHoAXAdxQBkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9\ncMY1cCEBxpwQTJiJHbX9LcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MU\nS89+DsixFf3HL+iWjr6yVnQ/mAGVPQ+HD4pwmQIDAQABAoIBAALpcFb59MBZZHJ3\nui9RRi96ig6kPQoRjkjN83pjM+/h/bANMmUOQU5FHBKLwj5uhN5Dpk2fzAnIX2TE\nVgfyNGsYuWLsIM+m6EJfm7pXJwJDr3RCpm+6DIKr1U8TwlR2OhbDi6fOlfH66q79\n2Klq4SXsa0vgfllpTVCDtydFVjwAuQV7Cf6DGRjbNpN3DPLeOC1wYFimNZwudSK0\nf8grWpPFXw2TPaf3TgeBGxwL7GCTYSKT+Eq9USbhG4RArrM9oQt+h7rzaH2bFEdg\n7tOM4KIgV+aw8r0TsYisDG9dfiHfHr5vQnkmWgt/rxAOvHlJ7/64pBVuET1ZF0mB\nP6gu4Y0CgYEAzkwXvfnHI5qx9BVP6e9lGrpWrm0RxCKr2iCCwrOVALbX1yfKCb5L\nrP/jSERMuLt6bIKg/AoVu9ogCTGzntyHTbZXFGg/y5Xoul+1af2arQ1rGZ7A/Im7\nnteZePg2U6UiDRy07F94FF5aL/v97D4BffiSA+0atlgH6tpKyYfY6NsCgYEA8+Ku\nGQqX9kHDd5bbzPhLelNmHVnAjnMaHEhvzVtBA737F10Oqg9wyffqe/i/DvdUSx9r\nafKGUfzB2vVZjz//OpSQ8VhRzDTiyelKLsSTmzOokLBnwayyTxw85o9EDvTNrzfb\nYQbAjmAXWmnv5Xvx1KfvTaKFY3BmHsKYJDzwnJsCgYBK1SVjn2CSVMIqlTSI2nMl\nb+STnzLrn9wQ4uwr7nKlcK34+RD72dCfr67lfwkJldBB3lzBMHNT0jr+us26Waqn\nEPaji3Fgyz9BpAgtq3XZQl3QTFsbAGdTpkegrwEd9G/Wq8whVjw7v0Id193zPUbT\nSEDHNdITxPkSQx8P3bxcMwKBgQDO5EGk5KO9OFTFoqib3RbKku1RgM4lCefgjmKp\n5vvkXMohK8RA6BBahYHZ4U7TN2W+xMyueBsSekVJplFvgG7YFyhOVQovHb42Yz2X\nJxPA2bXp6HxchFBPZDkVrfuiZHIIbm4ghUXcgg/Nl4j3OIoSSNRtG63kiXlYJuRB\n+aB0eQKBgD79VrREpbOMS7HRlDTtfkDN94HY3T4MLErs26z/NLO/dC44tmBJGo2P\ngcQ+p7XxNjpWUnUbEiuz4R3Xgh6ULwuSseWtcQicolPHTkBjnc+6BEpyguZJ+FPZ\nGls3g3LxjGhdPlyd37CaWDvx/Jtjrd4Y9iGkGO2d9fXZD0Hg0ymX\n-----END RSA PRIVATE KEY-----", + "cert": "-----BEGIN CERTIFICATE-----\nMIIG5DCCBMygAwIBAgIQBPQGlt81+4RKt3RAFXPvrjANBgkqhkiG9w0BAQsFADBb\nMQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywg\nSW5jLjElMCMGA1UEAxMcVHJ1c3RBc2lhIERWIFRMUyBSU0EgQ0EgMjAyNTAeFw0y\nNTA0MjIwMDAwMDBaFw0yNTA3MjAyMzU5NTlaMB8xHTAbBgNVBAMTFGFsbGluc3Ns\nLnphY2h5YW5nLmNuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxIjm\nAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONhWhMT6W+cx0WMC80yCRm5\nJshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM4tz/Zh8a3kVyN4MtWDmV\n1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1z6qqEDcM8FtHoAXAdxQB\nkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9cMY1cCEBxpwQTJiJHbX9\nLcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MUS89+DsixFf3HL+iWjr6y\nVnQ/mAGVPQ+HD4pwmQIDAQABo4IC3jCCAtowHwYDVR0jBBgwFoAUtBIopbTAHZ8p\ncWk82RGWSnVpUMAwHQYDVR0OBBYEFHqqdlMVBlcadf7iJLJoLnLZ7h4tMB8GA1Ud\nEQQYMBaCFGFsbGluc3NsLnphY2h5YW5nLmNuMD4GA1UdIAQ3MDUwMwYGZ4EMAQIB\nMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNV\nHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHkGCCsG\nAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t\nMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vVHJ1c3RB\nc2lhRFZUTFNSU0FDQTIwMjUuY3J0MAwGA1UdEwEB/wQCMAAwggF9BgorBgEEAdZ5\nAgQCBIIBbQSCAWkBZwB2ABLxTjS9U3JMhAYZw48/ehP457Vih4icbTAFhOvlhiY6\nAAABll0w/o0AAAQDAEcwRQIgd24jCPm+fbHq3grMIxtvQhzkv7dvYPM/BGjPEsy1\nQ70CIQC5jXADjBh+dH50T+atn3lktBEqQhedOl6cAaP/XXmk6gB2AO08S9boBsKk\nogBX28sk4jgB31Ev7cSGxXAPIN23Pj/gAAABll0w/rUAAAQDAEcwRQIgU2GDVEH1\ns5i/RC1RhqvJjn72PAZOlDtJyLdg29vC9HECIQCj78GATYK5quitLxbn3HvD8BeT\noOz+3tacgyN6+TdvugB1AKRCxQZJYGFUjw/U6pz7ei0mRU2HqX8v30VZ9idPOoRU\nAAABll0w/sYAAAQDAEYwRAIgCvU/iBRPKoJLjmU4edBYObWAO/aJp2mWnfJ4ieAr\nrXsCIBsAppYu28h8YEOl0N9yEeF9G05IMxwkCjZKonQs2SKMMA0GCSqGSIb3DQEB\nCwUAA4ICAQB3wFou51Qvl4apMhencuQUnWF3UpYP49e0WQ72DVT3pYjYsozkSuqb\nQZcwMB6HDoHdFicxvQ/yxKyTu/nw3rXjUWYuSxXYd7lJcQ/R0tR00m6AFeinY4Aq\nq4QqoA+lriK1XqO5MomAL4FbSysT1ow/gaG9pYuXEdT4pr05I/NumjXdkwBRZOd4\nrhol2grKf3y37Qla5hUbbG3ab9nf/csJSWkCoESeXr3MB1oAU/aL9pGSagvMXSKQ\nsFs2cn2Fi8ZmJPJXIP114lgvFuFDO+C1yTNbHap/FufvAKGryfPDuPecCF6FSXej\n+bwg4/BNz5lcHbNo2XXjLgoPg4VE6mG/SQQZQEDBk5DowwMVMvh77t9RBNrHozah\nHGtQz2hCuIX7rZQYnSlvW8T75FhI/Sd+HEfU/iyTIELXBUjypnK2bOJL7+jE7f79\nuljhXlCcP52fGHCjexNBz5gIZr82KVxsfxKuZjfioPkhmWleVNMdMWYJRXu618E6\nNtNjUVsDCuMOOMNs1qScqxOT60MeDZLX+vnC93fdd/t2hLEAWWNNMkWeX2qLCE1q\nGarop9U1mJpiBWkW5cBiqnNIbhuV2fcwFIR8mVT5f1Qcw+WxE2nEjY2h75bKv8T5\n3RBngmaX8PcyLAP2s0/4UyzAnMYfioJBh37VpUYBrdriBkRds/AMZw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFnjCCBIagAwIBAgIQCSYyO0lk42hGFRLe8aXVLDANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0yNTAxMDgwMDAwMDBaFw0zNTAxMDcyMzU5NTlaMFsxCzAJBgNVBAYTAkNO\nMSUwIwYDVQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSUwIwYDVQQD\nExxUcnVzdEFzaWEgRFYgVExTIFJTQSBDQSAyMDI1MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA0fuEmuBIsN6ZZVq+gRobMorOGIilTCIfQrxNpR8FUZ9R\n/GfbiekbiIKphQXEZ7N1uBnn6tXUuZ32zl6jPkZpHzN/Bmgk1BWSIzVc0npMzrWq\n/hrbk5+KddXJdsNpeG1+Q8lc8uVMBrztnxaPb7Rh7yQCsMrcO4hgVaqLJWkVvEfW\nULtoCHQnNaj4IroG6VxQf1oArQ8bPbwpI02lieSahRa78FQuXdoGVeQcrkhtVjZs\nON98vq5fPWZX2LFv7e5J6P9IHbzvOl8yyQjv+2/IOwhNSkaXX3bI+//bqF9XW/p7\n+gsUmHiK5YsvLjmXcvDmoDEGrXMzgX31Zl2nJ+umpRbLjwP8rxYIUsKoEwEdFoto\nAid59UEBJyw/GibwXQ5xTyKD/N6C8SFkr1+myOo4oe1UB+YgvRu6qSxIABo5kYdX\nFodLP4IgoVJdeUFs1Usa6bxYEO6EgMf5lCWt9hGZszvXYZwvyZGq3ogNXM7eKyi2\n20WzJXYMmi9TYFq2Fa95aZe4wki6YhDhhOO1g0sjITGVaB73G+JOCI9yJhv6+REN\nD40ZpboUHE8JNgMVWbG1isAMVCXqiADgXtuC+tmJWPEH9cR6OuJLEpwOzPfgAbnn\n2MRu7Tsdr8jPjTPbD0FxblX1ydW3RG30vwLF5lkTTRkHG9epMgpPMdYP7nY/08MC\nAwEAAaOCAVYwggFSMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLQSKKW0\nwB2fKXFpPNkRlkp1aVDAMB8GA1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485\nMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\ndgYIKwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy\ndC5jb20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E\naWdpQ2VydEdsb2JhbFJvb3RHMi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov\nL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDARBgNV\nHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQELBQADggEBAJ4a3svh316GY2+Z7EYx\nmBIsOwjJSnyoEfzx2T699ctLLrvuzS79Mg3pPjxSLlUgyM8UzrFc5tgVU3dZ1sFQ\nI4RM+ysJdvIAX/7Yx1QbooVdKhkdi9X7QN7yVkjqwM3fY3WfQkRTzhIkM7mYIQbR\nr+y2Vkju61BLqh7OCRpPMiudjEpP1kEtRyGs2g0aQpEIqKBzxgitCXSayO1hoO6/\n71ts801OzYlqYW9OQQQ2GCJyFbD6XHDjdpn+bWUxTKWaMY0qedSCbHE3Kl2QEF0C\nynZ7SbC03yR+gKZQDeTXrNP1kk5Qhe7jSXgw+nhbspe0q/M1ZcNCz+sPxeOwdCcC\ngJE=\n-----END CERTIFICATE-----", + "issuer": "cert-issuer", + }, + } + err := DeployAliCdn(cfg) + if err != nil { + t.Errorf("DeployAliCdn failed: %v", err) + } else { + t.Logf("DeployAliCdn succeeded") + } +} + +func TestALiOss(t *testing.T) { + cfg := map[string]any{ + "domain": "befunny.cn", + "region": "cn-beijing", + "bucket": "bt-test", + "provider_id": "25", + "certificate": map[string]any{ + "key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6F44tk6411wYH\n2BbL8V8VuKGCeH2mkfVl5orYz1gnLmVimyEzeIOyCnjYKpoP4kzC7SmqyMGwedN4\nvVTVcrloDRB0VziUIO+AJlv+ELeFPiQguycx0Jn6bDKZBYHo/ZIWMuUsxwnEyHfJ\nnG74GLVbk2la2CAeO6RPzxkI5/PezrGSWlttFoZYcxig89OVxA6N2XYirICP5euU\nsOkjO8oMMst729/S5gGlhEKYl8KJYr0NQ8SfVOwqUDnHgQEDMAXnJL7Pd8UlGU0V\nvSpwfhET+e9fbCU7O2iqgrxATUeCr/eYkfBxHqzZFqRdbQ0dvH5MKtK6uPfnc1sH\n7OCALpeJAgMBAAECggEAWFQD7UgyoWWNfD2qHGVWD5ZSOv58DYssIpD6CIzqN7bC\n8rnVWXvzbpef4mLeO3nbm448f87IeL5qjN25HZNVw7invcEEnvK/G2GZuo8uvLTR\nKyQKJ4/u9jlTDuTZU8DADX9c3hMfZOMOUIjK90GrG2ttz2vUWuVOSX9wT5ThYTiE\nk9jv3L07yjegYkoTpDf/pXSXxAkjrjUeuo39H5FgQf0VD20RKafoqGPgT4fGp1i2\nDv5lMTPsQCCEtuJyJEGRlOLldPYaZb2gsw98/jkJNfr/5kE9GixpazbgtfIJzX7c\nj7qvsf8Ula0HGA5gW54upogxHWgBZKNe0jMz2aOXPQKBgQDs9VmlnhYQDvGIIoMO\nwSdiG0TCRpva988d2nV2vG7DxtURoyJu2bzmXJMlExKLtSZP/OZBNd1neUBb16NR\neK+z54cW7kgVdYV1Pb+6MFluC9wh2wJOVHMNvhuI/wFlc/gbiyClisBk//XXcVsc\nG5N/lbKCIoosAnaEbXc2/xtgfwKBgQDJC8tgTABVkg4r4xPEHLLUN8DYOsO1yYa6\nPghTrxPuw5kaQLrRrI7t+E++Dd5fbuScu4e8+Ti7A+U43xN9skkqnad+4XHsgRq7\nWu44rZS0a7wVtsUDTiron/J4qlu6KfgoTb0kUEkszriGPKaqF/CJjKoj3IEixyYb\nM3tQw90D9wKBgGtrtp48EmhpPdmnO56etcnl7r/b3p/fo4c3F/Uh61zZcJI0UFHM\nZ7RO124BPXEUSDAOyBtb3ekgsKpyEVnHym9WUIl2sDr6Mew6eAZiEMiwm7TFYkA8\nTIQ4YKc0Y1+ouRtTcRNa2WlwF/T5MIKHhdBa/re8DMNywmO6dEb8U17lAoGAQU37\negQ195XB1K+mNAW+cQDLO3GbMOmNQeH0gnpUVzJiAQ0VohYTN2l5PZrzqLw0tlST\n+uZZbyYMxzRu+F15NsaPKb/BablmHYWj6/U2YIS+S69av4AcoAOUl21+7jHD0hOu\nZKVPn6ZmefQpjwbHs2ZlvdBaghl+X0eRvuJgYHECgYAiv6eMzqHGc0jfs8uZqGGE\nv/DVDTRyevnZMQEw8ZzVwfuTxZM6fKanjingkG3a56ELKiPX8TTFsmKpJK/AmjfB\njCeloQV3bSHc+Tas4duArgZBjBLEO1awM4FlSf7ntItTLf2F0JiMMS0loIqC8uwn\nWkU6gr8Q3Oh8t8iv1HTdoA==\n-----END PRIVATE KEY-----", + "cert": "-----BEGIN CERTIFICATE-----\nMIIG9TCCBN2gAwIBAgIQUUIS2m0nbRAAeXaCB2b1NDANBgkqhkiG9w0BAQsFADBX\nMQswCQYDVQQGEwJDTjEtMCsGA1UECgwk5bm/5Lic5aCh5aGU5a6J5YWo5oqA5pyv\n5pyJ6ZmQ5YWs5Y+4MRkwFwYDVQQDDBDlrp3loZQgRFYgVExTIENBMB4XDTI1MDQx\nMDA2Mzg0NloXDTI2MDQxMDA2Mzg0NVowFzEVMBMGA1UEAwwMKi5iZWZ1bm55LmNu\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuheOLZOuNdcGB9gWy/Ff\nFbihgnh9ppH1ZeaK2M9YJy5lYpshM3iDsgp42CqaD+JMwu0pqsjBsHnTeL1U1XK5\naA0QdFc4lCDvgCZb/hC3hT4kILsnMdCZ+mwymQWB6P2SFjLlLMcJxMh3yZxu+Bi1\nW5NpWtggHjukT88ZCOfz3s6xklpbbRaGWHMYoPPTlcQOjdl2IqyAj+XrlLDpIzvK\nDDLLe9vf0uYBpYRCmJfCiWK9DUPEn1TsKlA5x4EBAzAF5yS+z3fFJRlNFb0qcH4R\nE/nvX2wlOztoqoK8QE1Hgq/3mJHwcR6s2RakXW0NHbx+TCrSurj353NbB+zggC6X\niQIDAQABo4IC+zCCAvcwDAYDVR0TAQH/BAIwADBHBgNVHR8EQDA+MDygOqA4hjZo\ndHRwOi8vYnRkdnRsc3IzNWcyY2EuY3JsLmNlcnR1bS5wbC9idGR2dGxzcjM1ZzJj\nYS5jcmwwgY0GCCsGAQUFBwEBBIGAMH4wMQYIKwYBBQUHMAGGJWh0dHA6Ly9idGR2\ndGxzcjM1ZzJjYS5vY3NwLWNlcnR1bS5jb20wSQYIKwYBBQUHMAKGPWh0dHA6Ly9i\ndGR2dGxzcjM1ZzJjYS5yZXBvc2l0b3J5LmNlcnR1bS5wbC9idGR2dGxzcjM1ZzJj\nYS5jZXIwHwYDVR0jBBgwFoAU20yJOMQn62M/cvkK1OlC3eyZc6gwIQYDVR0gBBow\nGDAIBgZngQwBAgEwDAYKKoRoAYb2dwJlATATBgNVHSUEDDAKBggrBgEFBQcDATAO\nBgNVHQ8BAf8EBAMCBaAwIwYDVR0RBBwwGoIMKi5iZWZ1bm55LmNuggpiZWZ1bm55\nLmNuMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdwAZhtTHKKpv/roDb3gqTQGR\nqs4tcjEPrs5dcEEtJUzH1AAAAZYebCyhAAAEAwBIMEYCIQC0TGbOoADw+Xh0F1b5\nIeAQDz3aWPOcEchBGRKhf7vb9wIhAO9+zS4YiwH1uMgW9WVgLagFouUv4/zohp6N\nrCmERgXmAHUADleUvPOuqT4zGyyZB7P3kN+bwj1xMiXdIaklrGHFTiEAAAGWHmws\nvwAABAMARjBEAiBmIxJi880176inXlzMZyVMU3rA6CsuQHHGfTx/9wbL8AIgVnaU\n0YfZm6GzTR3+/bt6b9PtmJ5GErBlHxHpbH5jAPEAdgBkEcRspBLsp4kcogIuALyr\nTygH1B41J6vq/tUDyX3N8AAAAZYebCzGAAAEAwBHMEUCIQDYdirqdSr8960U+kE1\ntPYwS8VWRN2zrhiRvu1zlEzX/AIgHE/LB5fRIZIFKHTmE1itu5z1jRg5RSiaAxzG\nS7XqlLkwDQYJKoZIhvcNAQELBQADggIBAASke6sdSSoI3tz8JlDl4+hLodjoud16\nvftwAmz28qpOTcXeNZwDN3aguKDK7lZlqQ8XCM7vV+8uWg4i/IBexEwQPvc51vFG\n/y0uuL2ybTsJun9DQUzINr8j3CrZw0wtnfbSoRLnWekCI0eV1rX5N2RBVeSR5eXI\nX6TuAQh4L0/AWjqF8pDq/GN0OD5Hw1bZagcWIE0i19HthLVjIjZevC9SSqHKqTNr\nZIHQemNh5k2sEDAg9mDlAza4UaX80yn4Lhhfi3uhQ6qt1GTZg1yTBuVYOob/WOvV\nNrU/m5yfyVQr/Tv2v3iz68AKIUcPsok84pqzlRM4u+t1Fg7lVziYNj3BqIcOuuz6\nGfU25Jh/nassyCdRyyc3xpvdL5YXPWOxFaA3Sg7jBpkHT3tJ/JRPjWwBF6Zfrdh0\ncWBidS9udC4x23NIFOopr4ElpBawafTBvZSWsErire5IcE9oPTJHaf4abOsg7XVT\nhyPRw+qPHvy028eA4uYEXXB42KdK46QVUAeq6AxWHXlD58ie5VIPd0QqmPd6y4yJ\nX/YTOZo0CY4AkY7c/uDWAaJrP+l3HV/5EezhtvrDSW8H9AAX1ruBf7htx9BM4PSs\n9y8n3tyyA/0NVQ0ixeYFcDM5Cwlbpdyxgd7bOtEGu7XvStSSbqMdiG8FuGwZxHr5\nHIOucga96oD8\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGmDCCBICgAwIBAgIRANIa0kwTJVdUiIuxnJd6d1QwDQYJKoZIhvcNAQENBQAw\nejELMAkGA1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5B\nLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR8wHQYD\nVQQDExZDZXJ0dW0gVHJ1c3RlZCBSb290IENBMB4XDTI1MDExNTEwMzQ1NVoXDTM1\nMDEwMzEwMzQ1NVowVzELMAkGA1UEBhMCQ04xLTArBgNVBAoMJOW5v+S4nOWgoeWh\nlOWuieWFqOaKgOacr+aciemZkOWFrOWPuDEZMBcGA1UEAwwQ5a6d5aGUIERWIFRM\nUyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJrqOp1sGzlYxNQz\nVz8BmnBbdMElduQwM0vi///FObjpBVCPiLMyeHr8UUacE6IcRbTE1hRO/lbKwYts\naFaSuUdLLreijqTMYCDPvXqIhurPkTRiDrLLnfRmjWhVcKehwBk8x9pOWMTx5Mbx\nU8f2edxwuUA/bf6Mq97EgabuCeP/y9WKhYNr695+hAwtT/j1p5nhcCuuKbLcVPuF\nM12PZCbeLOANe9BKKwRGu3j6fzKL+m4IDNuXm9Ca203oqLq1QllXkDrkOwTksrny\nOmD/Hn4cPWyCj/0TUZQN7CXfwq05yyfxwZXSS9POQkThIqk1gQhGSasm5rJbGYgk\nSjp0nAEr06N4NdR68DeLstq4tFMuPWNPI0IeWw4afBepEHSoDXO/HeDsoRvAqAPn\nhmjgKzS3K5QHr3KDJuVFn3jXu9+VYcbgrGMuDGRKnPLxnNRT3uf8qloFk2OZvUJZ\nytX1134tuyGn4YnjIxM/6Tm4plHSbEBMmlY6or4oHqAD55528dojR9dPsx1QCKw7\neARjPC5pVtLI8vi/SyW69BXwEK4cy2D8Z32qpsSlxAFFxorKM3i764pwAxQdDLAh\nZKQjnIINJVqG/62IPbeewnvoP8XRtgNa5WoE4ChmD0XvMlH565b25wBTzKZ9hXnp\nHiq13zbJ+VJ/3PcGLRcK9HHAiyzpAgMBAAGjggE6MIIBNjBxBggrBgEFBQcBAQRl\nMGMwNwYIKwYBBQUHMAKGK2h0dHA6Ly9zdWJjYS5yZXBvc2l0b3J5LmNlcnR1bS5w\nbC9jdHJjYS5jZXIwKAYIKwYBBQUHMAGGHGh0dHA6Ly9zdWJjYS5vY3NwLWNlcnR1\nbS5jb20wHwYDVR0jBBgwFoAUjPscdbwC059OLkjZ+WBUqsSzT/owEgYDVR0TAQH/\nBAgwBgEB/wIBADA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8vc3ViY2EuY3JsLmNl\ncnR1bS5wbC9jdHJjYS5jcmwwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDgYDVR0PAQH/\nBAQDAgEGMBEGA1UdIAQKMAgwBgYEVR0gADAdBgNVHQ4EFgQU20yJOMQn62M/cvkK\n1OlC3eyZc6gwDQYJKoZIhvcNAQENBQADggIBAJGal9WZ1oHFDeocAP/U9eQF+dFy\nbjIBpe9/j9ZOx/VIGrRyVgZlQ6kqxciGmJG2lTTZg0qG1E9SZ4WEMhI1Ju0mkEyO\n7dsygB/AP6XDsKwvq4+gytQ2lIkaWrviKpAf3yXDaPFkK+fryc1a7CJjdNNxQPCJ\nGtOTc/2qOPUP1RwYlLPOdnpW922o4J6n+zaB0210geoHKdxIRF0SAia3HYcepeyA\nl2n92mL4Af+sNkwRMZoTgOac0Dz81qlApZs8ueqTKmHH5qcy7omtAoI41L+dEPHc\nupL7990zZIEjK1KulpqZWAV0bjaZvUcSPqdE8UPw+K/0Zg9YTt+4HI2rmMCEmVK5\nQAlp0XWWFLvQyvIsnDAqIh20lEz+ryn+cYMSdGuy07ipE1e3uKlPPxYe76dJJ2Cu\n22QdI5XSaX+SwDtn5UfMeWglM7l3g8Ef82MkMxqGItFmmu0GC20Dj2x57QSGsgHo\nTNFH3Kzq7nzI5I5WmZgj7lbXsAm6EsfFDS2Nw4q+gKM8kxNv1yM+q45HOI9BU30W\nK0Nrwhqoe5Ht5Pmbcpj/NfrutqJ3YrjIJC86okjFELMwkwqTe5wcrygQ5vFLWDtB\n1wQBv/6vSLyszw9xOi5QYLTS9NIPP45sqrBrasnBMEdqkmv4dEoOZn5rwmQ3EOzL\nNoVPcXhpL4qk5KQG\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFxDCCBKygAwIBAgIRANjgdEtYJJGfvQiEffcgIPowDQYJKoZIhvcNAQENBQAw\nfjELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu\nQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEiMCAG\nA1UEAxMZQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQTAeFw0yMzA5MTkxMDAwMDBa\nFw0yODA5MTkxMDAwMDBaMHoxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28g\nRGF0YSBTeXN0ZW1zIFMuQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9u\nIEF1dGhvcml0eTEfMB0GA1UEAxMWQ2VydHVtIFRydXN0ZWQgUm9vdCBDQTCCAiIw\nDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANEtjru3NuptN5GfTpOnBeQpAyXO\nHIL3fJmfQQbN7aO6wNsJLMF83yl+S2Uvk6fUAWsDKBij2J0FwSrYRfGR3t870IAC\njM84D+qnXHgRpMHIhVwl09Oy5yXPEVSXqzXAHnYc7wBTnzncFKUsIiWzcnL8jbPl\nPggeFCo3C4g8yrD0yMKhrrzBvilnVeL8rVlc/r1XLLCQjcLtN7Z8mYi11QOaPRUN\nPTqoqEXwlU4lWR3NmGm708wyyY3vgf6tfYm7umATymWVZ6DzGfYDVtRq0yfioa2D\n8EoSInccBXPiGXFCwOx1RpqQWOBqjiulRjAEjhmyF+O+qbp/VvEkA9eyISh2DjYw\nTHnVQZqaqLg1ugw68kQbIIj3xSXXPcbjPkPdh/7E6vVTPkxl/ztKy3haaxdfDcfD\nT06aKqLtV00i4kaaPw+RNCR9VeOMlTfTGvAJKyzSyY20DQCrZyko2AH1GQS2Hb52\n/nJcxIXK0oBB3wWoo9WEkE8L8+A/mxnSN4k/8ntSHIz24fc8B5eMDqJZgQyykD3T\n41lG7Q+pp96Aa1qqB7YZy7xX85chegyxK3Q+69qnZy1MxJieNgl2Zmb8Gj/qSFQc\nvjC9gFC/fLXOAPYMYdnnJAPg4wGBDr3YhTSIvbI2qHtcCOVEgIxv+C/VIcodHND7\nxLWH0TpOx3a1NUi1AgMBAAGjggE/MIIBOzAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBSM+xx1vALTn04uSNn5YFSqxLNP+jAfBgNVHSMEGDAWgBQIds3LB/8k9sXN\n7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYwLwYDVR0fBCgwJjAkoCKgIIYeaHR0\ncDovL2NybC5jZXJ0dW0ucGwvY3RuY2EuY3JsMGsGCCsGAQUFBwEBBF8wXTAoBggr\nBgEFBQcwAYYcaHR0cDovL3N1YmNhLm9jc3AtY2VydHVtLmNvbTAxBggrBgEFBQcw\nAoYlaHR0cDovL3JlcG9zaXRvcnkuY2VydHVtLnBsL2N0bmNhLmNlcjA6BgNVHSAE\nMzAxMC8GBFUdIAAwJzAlBggrBgEFBQcCARYZaHR0cHM6Ly93d3cuY2VydHVtLnBs\nL0NQUzANBgkqhkiG9w0BAQ0FAAOCAQEAKPpdjKpRGqa+eIZpwndaFy+p4H1FT2wf\n9JOJdmyjSkJ4wlUp+xbL6v7D6/6xYO8IcvyWyR1sdqef8HK49mYK9jDjk+ypq7IO\nh8R9VbhSuS6KxY3X5lVV6vmYZ6QE4G8IuK9ktvGSVS4uNabf+/r+Gq9oiysCwsck\nAsjwXbYN2gsKAWxXGC1vAqIOlB3tJaL29UVh2nKdl0eD7EsFwbk0vo/U5pQ/Yi8u\nJJb/OSiK1XXUJ7S8wmURNWK2hIzHSnP9xotjmUJMVMb8Igj8jntZNc9U3lNuzG6R\nF2kSR3FCgnCJ9AebGFZwEc7QDTCuohG6Lmb6D3Dtopv/LvqypxAmQQ==\n-----END CERTIFICATE-----", + "issuer": "cert-issuer", + }, + } + err := DeployOss(cfg) + if err != nil { + t.Errorf("DeployAliCdn failed: %v", err) + } else { + t.Logf("DeployAliCdn succeeded") + } +} diff --git a/backend/internal/cert/deploy/aliyun.go b/backend/internal/cert/deploy/aliyun.go new file mode 100644 index 0000000..1433327 --- /dev/null +++ b/backend/internal/cert/deploy/aliyun.go @@ -0,0 +1,186 @@ +package deploy + +import ( + "ALLinSSL/backend/internal/access" + "encoding/json" + "fmt" + aliyuncdn "github.com/alibabacloud-go/cdn-20180510/v6/client" + aliyunopenapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + "github.com/alibabacloud-go/tea/tea" + "strconv" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +func ClientAliCdn(accessKey, accessSecret string) (_result *aliyuncdn.Client, err error) { + config := &aliyunopenapi.Config{ + AccessKeyId: tea.String(accessKey), + AccessKeySecret: tea.String(accessSecret), + Endpoint: tea.String("cdn.aliyuncs.com"), + } + client, err := aliyuncdn.NewClient(config) + if err != nil { + return nil, err + } + + return client, nil +} + +func DeployAliCdn(cfg map[string]any) error { + cert, ok := cfg["certificate"].(map[string]any) + if !ok { + return fmt.Errorf("证书不存在") + } + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return fmt.Errorf("参数错误:provider_id") + } + // + providerData, err := access.GetAccess(providerID) + if err != nil { + return err + } + providerConfigStr, ok := providerData["config"].(string) + if !ok { + return fmt.Errorf("api配置错误") + } + // 解析 JSON 配置 + var providerConfig map[string]string + err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) + if err != nil { + return err + } + + client, err := ClientAliCdn(providerConfig["access_key"], providerConfig["access_secret"]) + if err != nil { + return err + } + domain, ok := cfg["domain"].(string) + if !ok { + return fmt.Errorf("参数错误:domain") + } + // 设置证书 + keyPem, ok := cert["key"].(string) + if !ok { + return fmt.Errorf("证书错误:key") + } + certPem, ok := cert["cert"].(string) + if !ok { + return fmt.Errorf("证书错误:cert") + } + + setCdnDomainSSLCertificateRequest := &aliyuncdn.SetCdnDomainSSLCertificateRequest{ + DomainName: tea.String(domain), + SSLProtocol: tea.String("on"), + SSLPub: tea.String(strings.TrimSpace(certPem)), + SSLPri: tea.String(strings.TrimSpace(keyPem)), + } + _, err = client.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateRequest) + if err != nil { + return err + } + return nil +} + +func ClientOss(accessKeyId, accessKeySecret, region string) (*oss.Client, error) { + // 接入点一览 https://api.aliyun.com/product/Oss + var endpoint string + switch region { + case "": + endpoint = "oss.aliyuncs.com" + case + "cn-hzjbp", + "cn-hzjbp-a", + "cn-hzjbp-b": + endpoint = "oss-cn-hzjbp-a-internal.aliyuncs.com" + case + "cn-shanghai-finance-1", + "cn-shenzhen-finance-1", + "cn-beijing-finance-1", + "cn-north-2-gov-1": + endpoint = fmt.Sprintf("oss-%s-internal.aliyuncs.com", region) + default: + endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", region) + } + + client, err := oss.New(endpoint, accessKeyId, accessKeySecret) + if err != nil { + return nil, err + } + + return client, nil +} + +func DeployOss(cfg map[string]any) error { + cert, ok := cfg["certificate"].(map[string]any) + if !ok { + return fmt.Errorf("证书不存在") + } + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return fmt.Errorf("参数错误:provider_id") + } + // + providerData, err := access.GetAccess(providerID) + if err != nil { + return err + } + providerConfigStr, ok := providerData["config"].(string) + if !ok { + return fmt.Errorf("api配置错误") + } + // 解析 JSON 配置 + var providerConfig map[string]string + err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) + if err != nil { + return err + } + region, ok := cfg["region"].(string) + if !ok { + return fmt.Errorf("参数错误:region") + } + + client, err := ClientOss(providerConfig["access_key"], providerConfig["access_secret"], region) + if err != nil { + return err + } + domain, ok := cfg["domain"].(string) + if !ok { + return fmt.Errorf("参数错误:domain") + } + bucket, ok := cfg["domain"].(string) + if !ok { + return fmt.Errorf("参数错误:bucket") + } + // 设置证书 + keyPem, ok := cert["key"].(string) + if !ok { + return fmt.Errorf("证书错误:key") + } + certPem, ok := cert["cert"].(string) + if !ok { + return fmt.Errorf("证书错误:cert") + } + + putBucketCnameWithCertificateRequest := oss.PutBucketCname{ + Cname: domain, + CertificateConfiguration: &oss.CertificateConfiguration{ + Certificate: certPem, + PrivateKey: keyPem, + Force: true, + }, + } + err = client.PutBucketCnameWithCertificate(bucket, putBucketCnameWithCertificateRequest) + return err +} diff --git a/backend/internal/cert/deploy/bt_test.go b/backend/internal/cert/deploy/bt_test.go new file mode 100644 index 0000000..7f351db --- /dev/null +++ b/backend/internal/cert/deploy/bt_test.go @@ -0,0 +1,31 @@ +package deploy + +import "testing" + +func TestBTSite(t *testing.T) { + cfg := map[string]any{ + "siteName": "abcd.cn", + "provider_id": "19", + "certificate": map[string]any{ + "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxIjmAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONh\nWhMT6W+cx0WMC80yCRm5JshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM\n4tz/Zh8a3kVyN4MtWDmV1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1\nz6qqEDcM8FtHoAXAdxQBkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9\ncMY1cCEBxpwQTJiJHbX9LcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MU\nS89+DsixFf3HL+iWjr6yVnQ/mAGVPQ+HD4pwmQIDAQABAoIBAALpcFb59MBZZHJ3\nui9RRi96ig6kPQoRjkjN83pjM+/h/bANMmUOQU5FHBKLwj5uhN5Dpk2fzAnIX2TE\nVgfyNGsYuWLsIM+m6EJfm7pXJwJDr3RCpm+6DIKr1U8TwlR2OhbDi6fOlfH66q79\n2Klq4SXsa0vgfllpTVCDtydFVjwAuQV7Cf6DGRjbNpN3DPLeOC1wYFimNZwudSK0\nf8grWpPFXw2TPaf3TgeBGxwL7GCTYSKT+Eq9USbhG4RArrM9oQt+h7rzaH2bFEdg\n7tOM4KIgV+aw8r0TsYisDG9dfiHfHr5vQnkmWgt/rxAOvHlJ7/64pBVuET1ZF0mB\nP6gu4Y0CgYEAzkwXvfnHI5qx9BVP6e9lGrpWrm0RxCKr2iCCwrOVALbX1yfKCb5L\nrP/jSERMuLt6bIKg/AoVu9ogCTGzntyHTbZXFGg/y5Xoul+1af2arQ1rGZ7A/Im7\nnteZePg2U6UiDRy07F94FF5aL/v97D4BffiSA+0atlgH6tpKyYfY6NsCgYEA8+Ku\nGQqX9kHDd5bbzPhLelNmHVnAjnMaHEhvzVtBA737F10Oqg9wyffqe/i/DvdUSx9r\nafKGUfzB2vVZjz//OpSQ8VhRzDTiyelKLsSTmzOokLBnwayyTxw85o9EDvTNrzfb\nYQbAjmAXWmnv5Xvx1KfvTaKFY3BmHsKYJDzwnJsCgYBK1SVjn2CSVMIqlTSI2nMl\nb+STnzLrn9wQ4uwr7nKlcK34+RD72dCfr67lfwkJldBB3lzBMHNT0jr+us26Waqn\nEPaji3Fgyz9BpAgtq3XZQl3QTFsbAGdTpkegrwEd9G/Wq8whVjw7v0Id193zPUbT\nSEDHNdITxPkSQx8P3bxcMwKBgQDO5EGk5KO9OFTFoqib3RbKku1RgM4lCefgjmKp\n5vvkXMohK8RA6BBahYHZ4U7TN2W+xMyueBsSekVJplFvgG7YFyhOVQovHb42Yz2X\nJxPA2bXp6HxchFBPZDkVrfuiZHIIbm4ghUXcgg/Nl4j3OIoSSNRtG63kiXlYJuRB\n+aB0eQKBgD79VrREpbOMS7HRlDTtfkDN94HY3T4MLErs26z/NLO/dC44tmBJGo2P\ngcQ+p7XxNjpWUnUbEiuz4R3Xgh6ULwuSseWtcQicolPHTkBjnc+6BEpyguZJ+FPZ\nGls3g3LxjGhdPlyd37CaWDvx/Jtjrd4Y9iGkGO2d9fXZD0Hg0ymX\n-----END RSA PRIVATE KEY-----", + "cert": "-----BEGIN CERTIFICATE-----\nMIIG5DCCBMygAwIBAgIQBPQGlt81+4RKt3RAFXPvrjANBgkqhkiG9w0BAQsFADBb\nMQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywg\nSW5jLjElMCMGA1UEAxMcVHJ1c3RBc2lhIERWIFRMUyBSU0EgQ0EgMjAyNTAeFw0y\nNTA0MjIwMDAwMDBaFw0yNTA3MjAyMzU5NTlaMB8xHTAbBgNVBAMTFGFsbGluc3Ns\nLnphY2h5YW5nLmNuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxIjm\nAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONhWhMT6W+cx0WMC80yCRm5\nJshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM4tz/Zh8a3kVyN4MtWDmV\n1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1z6qqEDcM8FtHoAXAdxQB\nkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9cMY1cCEBxpwQTJiJHbX9\nLcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MUS89+DsixFf3HL+iWjr6y\nVnQ/mAGVPQ+HD4pwmQIDAQABo4IC3jCCAtowHwYDVR0jBBgwFoAUtBIopbTAHZ8p\ncWk82RGWSnVpUMAwHQYDVR0OBBYEFHqqdlMVBlcadf7iJLJoLnLZ7h4tMB8GA1Ud\nEQQYMBaCFGFsbGluc3NsLnphY2h5YW5nLmNuMD4GA1UdIAQ3MDUwMwYGZ4EMAQIB\nMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNV\nHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHkGCCsG\nAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t\nMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vVHJ1c3RB\nc2lhRFZUTFNSU0FDQTIwMjUuY3J0MAwGA1UdEwEB/wQCMAAwggF9BgorBgEEAdZ5\nAgQCBIIBbQSCAWkBZwB2ABLxTjS9U3JMhAYZw48/ehP457Vih4icbTAFhOvlhiY6\nAAABll0w/o0AAAQDAEcwRQIgd24jCPm+fbHq3grMIxtvQhzkv7dvYPM/BGjPEsy1\nQ70CIQC5jXADjBh+dH50T+atn3lktBEqQhedOl6cAaP/XXmk6gB2AO08S9boBsKk\nogBX28sk4jgB31Ev7cSGxXAPIN23Pj/gAAABll0w/rUAAAQDAEcwRQIgU2GDVEH1\ns5i/RC1RhqvJjn72PAZOlDtJyLdg29vC9HECIQCj78GATYK5quitLxbn3HvD8BeT\noOz+3tacgyN6+TdvugB1AKRCxQZJYGFUjw/U6pz7ei0mRU2HqX8v30VZ9idPOoRU\nAAABll0w/sYAAAQDAEYwRAIgCvU/iBRPKoJLjmU4edBYObWAO/aJp2mWnfJ4ieAr\nrXsCIBsAppYu28h8YEOl0N9yEeF9G05IMxwkCjZKonQs2SKMMA0GCSqGSIb3DQEB\nCwUAA4ICAQB3wFou51Qvl4apMhencuQUnWF3UpYP49e0WQ72DVT3pYjYsozkSuqb\nQZcwMB6HDoHdFicxvQ/yxKyTu/nw3rXjUWYuSxXYd7lJcQ/R0tR00m6AFeinY4Aq\nq4QqoA+lriK1XqO5MomAL4FbSysT1ow/gaG9pYuXEdT4pr05I/NumjXdkwBRZOd4\nrhol2grKf3y37Qla5hUbbG3ab9nf/csJSWkCoESeXr3MB1oAU/aL9pGSagvMXSKQ\nsFs2cn2Fi8ZmJPJXIP114lgvFuFDO+C1yTNbHap/FufvAKGryfPDuPecCF6FSXej\n+bwg4/BNz5lcHbNo2XXjLgoPg4VE6mG/SQQZQEDBk5DowwMVMvh77t9RBNrHozah\nHGtQz2hCuIX7rZQYnSlvW8T75FhI/Sd+HEfU/iyTIELXBUjypnK2bOJL7+jE7f79\nuljhXlCcP52fGHCjexNBz5gIZr82KVxsfxKuZjfioPkhmWleVNMdMWYJRXu618E6\nNtNjUVsDCuMOOMNs1qScqxOT60MeDZLX+vnC93fdd/t2hLEAWWNNMkWeX2qLCE1q\nGarop9U1mJpiBWkW5cBiqnNIbhuV2fcwFIR8mVT5f1Qcw+WxE2nEjY2h75bKv8T5\n3RBngmaX8PcyLAP2s0/4UyzAnMYfioJBh37VpUYBrdriBkRds/AMZw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFnjCCBIagAwIBAgIQCSYyO0lk42hGFRLe8aXVLDANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0yNTAxMDgwMDAwMDBaFw0zNTAxMDcyMzU5NTlaMFsxCzAJBgNVBAYTAkNO\nMSUwIwYDVQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSUwIwYDVQQD\nExxUcnVzdEFzaWEgRFYgVExTIFJTQSBDQSAyMDI1MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA0fuEmuBIsN6ZZVq+gRobMorOGIilTCIfQrxNpR8FUZ9R\n/GfbiekbiIKphQXEZ7N1uBnn6tXUuZ32zl6jPkZpHzN/Bmgk1BWSIzVc0npMzrWq\n/hrbk5+KddXJdsNpeG1+Q8lc8uVMBrztnxaPb7Rh7yQCsMrcO4hgVaqLJWkVvEfW\nULtoCHQnNaj4IroG6VxQf1oArQ8bPbwpI02lieSahRa78FQuXdoGVeQcrkhtVjZs\nON98vq5fPWZX2LFv7e5J6P9IHbzvOl8yyQjv+2/IOwhNSkaXX3bI+//bqF9XW/p7\n+gsUmHiK5YsvLjmXcvDmoDEGrXMzgX31Zl2nJ+umpRbLjwP8rxYIUsKoEwEdFoto\nAid59UEBJyw/GibwXQ5xTyKD/N6C8SFkr1+myOo4oe1UB+YgvRu6qSxIABo5kYdX\nFodLP4IgoVJdeUFs1Usa6bxYEO6EgMf5lCWt9hGZszvXYZwvyZGq3ogNXM7eKyi2\n20WzJXYMmi9TYFq2Fa95aZe4wki6YhDhhOO1g0sjITGVaB73G+JOCI9yJhv6+REN\nD40ZpboUHE8JNgMVWbG1isAMVCXqiADgXtuC+tmJWPEH9cR6OuJLEpwOzPfgAbnn\n2MRu7Tsdr8jPjTPbD0FxblX1ydW3RG30vwLF5lkTTRkHG9epMgpPMdYP7nY/08MC\nAwEAAaOCAVYwggFSMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLQSKKW0\nwB2fKXFpPNkRlkp1aVDAMB8GA1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485\nMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\ndgYIKwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy\ndC5jb20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E\naWdpQ2VydEdsb2JhbFJvb3RHMi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov\nL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDARBgNV\nHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQELBQADggEBAJ4a3svh316GY2+Z7EYx\nmBIsOwjJSnyoEfzx2T699ctLLrvuzS79Mg3pPjxSLlUgyM8UzrFc5tgVU3dZ1sFQ\nI4RM+ysJdvIAX/7Yx1QbooVdKhkdi9X7QN7yVkjqwM3fY3WfQkRTzhIkM7mYIQbR\nr+y2Vkju61BLqh7OCRpPMiudjEpP1kEtRyGs2g0aQpEIqKBzxgitCXSayO1hoO6/\n71ts801OzYlqYW9OQQQ2GCJyFbD6XHDjdpn+bWUxTKWaMY0qedSCbHE3Kl2QEF0C\nynZ7SbC03yR+gKZQDeTXrNP1kk5Qhe7jSXgw+nhbspe0q/M1ZcNCz+sPxeOwdCcC\ngJE=\n-----END CERTIFICATE-----", + "issuer": "cert-issuer", + }, + } + err := DeployBtSite(cfg) + println(err) +} + +func TestBTP(t *testing.T) { + cfg := map[string]any{ + "site_id": "1", + "provider_id": "19", + "certificate": map[string]any{ + "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxIjmAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONh\nWhMT6W+cx0WMC80yCRm5JshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM\n4tz/Zh8a3kVyN4MtWDmV1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1\nz6qqEDcM8FtHoAXAdxQBkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9\ncMY1cCEBxpwQTJiJHbX9LcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MU\nS89+DsixFf3HL+iWjr6yVnQ/mAGVPQ+HD4pwmQIDAQABAoIBAALpcFb59MBZZHJ3\nui9RRi96ig6kPQoRjkjN83pjM+/h/bANMmUOQU5FHBKLwj5uhN5Dpk2fzAnIX2TE\nVgfyNGsYuWLsIM+m6EJfm7pXJwJDr3RCpm+6DIKr1U8TwlR2OhbDi6fOlfH66q79\n2Klq4SXsa0vgfllpTVCDtydFVjwAuQV7Cf6DGRjbNpN3DPLeOC1wYFimNZwudSK0\nf8grWpPFXw2TPaf3TgeBGxwL7GCTYSKT+Eq9USbhG4RArrM9oQt+h7rzaH2bFEdg\n7tOM4KIgV+aw8r0TsYisDG9dfiHfHr5vQnkmWgt/rxAOvHlJ7/64pBVuET1ZF0mB\nP6gu4Y0CgYEAzkwXvfnHI5qx9BVP6e9lGrpWrm0RxCKr2iCCwrOVALbX1yfKCb5L\nrP/jSERMuLt6bIKg/AoVu9ogCTGzntyHTbZXFGg/y5Xoul+1af2arQ1rGZ7A/Im7\nnteZePg2U6UiDRy07F94FF5aL/v97D4BffiSA+0atlgH6tpKyYfY6NsCgYEA8+Ku\nGQqX9kHDd5bbzPhLelNmHVnAjnMaHEhvzVtBA737F10Oqg9wyffqe/i/DvdUSx9r\nafKGUfzB2vVZjz//OpSQ8VhRzDTiyelKLsSTmzOokLBnwayyTxw85o9EDvTNrzfb\nYQbAjmAXWmnv5Xvx1KfvTaKFY3BmHsKYJDzwnJsCgYBK1SVjn2CSVMIqlTSI2nMl\nb+STnzLrn9wQ4uwr7nKlcK34+RD72dCfr67lfwkJldBB3lzBMHNT0jr+us26Waqn\nEPaji3Fgyz9BpAgtq3XZQl3QTFsbAGdTpkegrwEd9G/Wq8whVjw7v0Id193zPUbT\nSEDHNdITxPkSQx8P3bxcMwKBgQDO5EGk5KO9OFTFoqib3RbKku1RgM4lCefgjmKp\n5vvkXMohK8RA6BBahYHZ4U7TN2W+xMyueBsSekVJplFvgG7YFyhOVQovHb42Yz2X\nJxPA2bXp6HxchFBPZDkVrfuiZHIIbm4ghUXcgg/Nl4j3OIoSSNRtG63kiXlYJuRB\n+aB0eQKBgD79VrREpbOMS7HRlDTtfkDN94HY3T4MLErs26z/NLO/dC44tmBJGo2P\ngcQ+p7XxNjpWUnUbEiuz4R3Xgh6ULwuSseWtcQicolPHTkBjnc+6BEpyguZJ+FPZ\nGls3g3LxjGhdPlyd37CaWDvx/Jtjrd4Y9iGkGO2d9fXZD0Hg0ymX\n-----END RSA PRIVATE KEY-----", + "cert": "-----BEGIN CERTIFICATE-----\nMIIG5DCCBMygAwIBAgIQBPQGlt81+4RKt3RAFXPvrjANBgkqhkiG9w0BAQsFADBb\nMQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywg\nSW5jLjElMCMGA1UEAxMcVHJ1c3RBc2lhIERWIFRMUyBSU0EgQ0EgMjAyNTAeFw0y\nNTA0MjIwMDAwMDBaFw0yNTA3MjAyMzU5NTlaMB8xHTAbBgNVBAMTFGFsbGluc3Ns\nLnphY2h5YW5nLmNuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxIjm\nAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONhWhMT6W+cx0WMC80yCRm5\nJshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM4tz/Zh8a3kVyN4MtWDmV\n1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1z6qqEDcM8FtHoAXAdxQB\nkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9cMY1cCEBxpwQTJiJHbX9\nLcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MUS89+DsixFf3HL+iWjr6y\nVnQ/mAGVPQ+HD4pwmQIDAQABo4IC3jCCAtowHwYDVR0jBBgwFoAUtBIopbTAHZ8p\ncWk82RGWSnVpUMAwHQYDVR0OBBYEFHqqdlMVBlcadf7iJLJoLnLZ7h4tMB8GA1Ud\nEQQYMBaCFGFsbGluc3NsLnphY2h5YW5nLmNuMD4GA1UdIAQ3MDUwMwYGZ4EMAQIB\nMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNV\nHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHkGCCsG\nAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t\nMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vVHJ1c3RB\nc2lhRFZUTFNSU0FDQTIwMjUuY3J0MAwGA1UdEwEB/wQCMAAwggF9BgorBgEEAdZ5\nAgQCBIIBbQSCAWkBZwB2ABLxTjS9U3JMhAYZw48/ehP457Vih4icbTAFhOvlhiY6\nAAABll0w/o0AAAQDAEcwRQIgd24jCPm+fbHq3grMIxtvQhzkv7dvYPM/BGjPEsy1\nQ70CIQC5jXADjBh+dH50T+atn3lktBEqQhedOl6cAaP/XXmk6gB2AO08S9boBsKk\nogBX28sk4jgB31Ev7cSGxXAPIN23Pj/gAAABll0w/rUAAAQDAEcwRQIgU2GDVEH1\ns5i/RC1RhqvJjn72PAZOlDtJyLdg29vC9HECIQCj78GATYK5quitLxbn3HvD8BeT\noOz+3tacgyN6+TdvugB1AKRCxQZJYGFUjw/U6pz7ei0mRU2HqX8v30VZ9idPOoRU\nAAABll0w/sYAAAQDAEYwRAIgCvU/iBRPKoJLjmU4edBYObWAO/aJp2mWnfJ4ieAr\nrXsCIBsAppYu28h8YEOl0N9yEeF9G05IMxwkCjZKonQs2SKMMA0GCSqGSIb3DQEB\nCwUAA4ICAQB3wFou51Qvl4apMhencuQUnWF3UpYP49e0WQ72DVT3pYjYsozkSuqb\nQZcwMB6HDoHdFicxvQ/yxKyTu/nw3rXjUWYuSxXYd7lJcQ/R0tR00m6AFeinY4Aq\nq4QqoA+lriK1XqO5MomAL4FbSysT1ow/gaG9pYuXEdT4pr05I/NumjXdkwBRZOd4\nrhol2grKf3y37Qla5hUbbG3ab9nf/csJSWkCoESeXr3MB1oAU/aL9pGSagvMXSKQ\nsFs2cn2Fi8ZmJPJXIP114lgvFuFDO+C1yTNbHap/FufvAKGryfPDuPecCF6FSXej\n+bwg4/BNz5lcHbNo2XXjLgoPg4VE6mG/SQQZQEDBk5DowwMVMvh77t9RBNrHozah\nHGtQz2hCuIX7rZQYnSlvW8T75FhI/Sd+HEfU/iyTIELXBUjypnK2bOJL7+jE7f79\nuljhXlCcP52fGHCjexNBz5gIZr82KVxsfxKuZjfioPkhmWleVNMdMWYJRXu618E6\nNtNjUVsDCuMOOMNs1qScqxOT60MeDZLX+vnC93fdd/t2hLEAWWNNMkWeX2qLCE1q\nGarop9U1mJpiBWkW5cBiqnNIbhuV2fcwFIR8mVT5f1Qcw+WxE2nEjY2h75bKv8T5\n3RBngmaX8PcyLAP2s0/4UyzAnMYfioJBh37VpUYBrdriBkRds/AMZw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFnjCCBIagAwIBAgIQCSYyO0lk42hGFRLe8aXVLDANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0yNTAxMDgwMDAwMDBaFw0zNTAxMDcyMzU5NTlaMFsxCzAJBgNVBAYTAkNO\nMSUwIwYDVQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSUwIwYDVQQD\nExxUcnVzdEFzaWEgRFYgVExTIFJTQSBDQSAyMDI1MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA0fuEmuBIsN6ZZVq+gRobMorOGIilTCIfQrxNpR8FUZ9R\n/GfbiekbiIKphQXEZ7N1uBnn6tXUuZ32zl6jPkZpHzN/Bmgk1BWSIzVc0npMzrWq\n/hrbk5+KddXJdsNpeG1+Q8lc8uVMBrztnxaPb7Rh7yQCsMrcO4hgVaqLJWkVvEfW\nULtoCHQnNaj4IroG6VxQf1oArQ8bPbwpI02lieSahRa78FQuXdoGVeQcrkhtVjZs\nON98vq5fPWZX2LFv7e5J6P9IHbzvOl8yyQjv+2/IOwhNSkaXX3bI+//bqF9XW/p7\n+gsUmHiK5YsvLjmXcvDmoDEGrXMzgX31Zl2nJ+umpRbLjwP8rxYIUsKoEwEdFoto\nAid59UEBJyw/GibwXQ5xTyKD/N6C8SFkr1+myOo4oe1UB+YgvRu6qSxIABo5kYdX\nFodLP4IgoVJdeUFs1Usa6bxYEO6EgMf5lCWt9hGZszvXYZwvyZGq3ogNXM7eKyi2\n20WzJXYMmi9TYFq2Fa95aZe4wki6YhDhhOO1g0sjITGVaB73G+JOCI9yJhv6+REN\nD40ZpboUHE8JNgMVWbG1isAMVCXqiADgXtuC+tmJWPEH9cR6OuJLEpwOzPfgAbnn\n2MRu7Tsdr8jPjTPbD0FxblX1ydW3RG30vwLF5lkTTRkHG9epMgpPMdYP7nY/08MC\nAwEAAaOCAVYwggFSMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLQSKKW0\nwB2fKXFpPNkRlkp1aVDAMB8GA1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485\nMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\ndgYIKwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy\ndC5jb20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E\naWdpQ2VydEdsb2JhbFJvb3RHMi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov\nL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDARBgNV\nHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQELBQADggEBAJ4a3svh316GY2+Z7EYx\nmBIsOwjJSnyoEfzx2T699ctLLrvuzS79Mg3pPjxSLlUgyM8UzrFc5tgVU3dZ1sFQ\nI4RM+ysJdvIAX/7Yx1QbooVdKhkdi9X7QN7yVkjqwM3fY3WfQkRTzhIkM7mYIQbR\nr+y2Vkju61BLqh7OCRpPMiudjEpP1kEtRyGs2g0aQpEIqKBzxgitCXSayO1hoO6/\n71ts801OzYlqYW9OQQQ2GCJyFbD6XHDjdpn+bWUxTKWaMY0qedSCbHE3Kl2QEF0C\nynZ7SbC03yR+gKZQDeTXrNP1kk5Qhe7jSXgw+nhbspe0q/M1ZcNCz+sPxeOwdCcC\ngJE=\n-----END CERTIFICATE-----", + "issuer": "cert-issuer", + }, + } + err := DeployBt(cfg) + println(err) +} diff --git a/backend/internal/cert/deploy/btpanel.go b/backend/internal/cert/deploy/btpanel.go new file mode 100644 index 0000000..2b7a6aa --- /dev/null +++ b/backend/internal/cert/deploy/btpanel.go @@ -0,0 +1,158 @@ +package deploy + +import ( + "ALLinSSL/backend/internal/access" + "crypto/md5" + "crypto/tls" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +func generateSignature(timestamp, apiKey string) string { + keyMd5 := md5.Sum([]byte(apiKey)) + keyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:])) + + signMd5 := md5.Sum([]byte(timestamp + keyMd5Hex)) + signMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:])) + return signMd5Hex +} + +func RequestBt(data *url.Values, method, providerID, requestUrl string) (map[string]any, error) { + providerData, err := access.GetAccess(providerID) + if err != nil { + return nil, err + } + providerConfigStr, ok := providerData["config"].(string) + if !ok { + return nil, fmt.Errorf("api配置错误") + } + // 解析 JSON 配置 + var providerConfig map[string]string + err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) + if err != nil { + return nil, err + } + timestamp := time.Now().Unix() + token := generateSignature(fmt.Sprintf("%d", timestamp), providerConfig["api_key"]) + if providerConfig["url"][len(providerConfig["url"])-1:] != "/" { + providerConfig["url"] += "/" + } + + data.Set("request_time", fmt.Sprintf("%d", timestamp)) + data.Set("request_token", token) + + req, err := http.NewRequest(method, providerConfig["url"]+requestUrl, strings.NewReader(data.Encode())) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36") + // 自定义 Transport,跳过 SSL 证书验证 + ignoreSsl := false + if providerConfig["ignore_ssl"] == "1" { + ignoreSsl = true + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl}, + } + + client := &http.Client{Transport: tr} + resp, err := client.Do(req) + if err != nil { + // fmt.Println(err) + return nil, fmt.Errorf("请求BT失败: %v", err) + } + body, _ := io.ReadAll(resp.Body) + defer resp.Body.Close() + + var res map[string]interface{} + err = json.Unmarshal(body, &res) + if err != nil { + return nil, fmt.Errorf("返回值解析失败: %v", err) + } + + if res["status"] != nil && !res["status"].(bool) { + return nil, fmt.Errorf("请求出错: %s", res["msg"].(string)) + } + return res, nil +} + +func DeployBt(cfg map[string]any) error { + cert, ok := cfg["certificate"].(map[string]any) + if !ok { + return fmt.Errorf("证书不存在") + } + // 设置证书 + keyPem, ok := cert["key"].(string) + if !ok { + return fmt.Errorf("证书错误:key") + } + certPem, ok := cert["cert"].(string) + if !ok { + return fmt.Errorf("证书错误:cert") + } + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return fmt.Errorf("参数错误:provider_id") + } + data := url.Values{} + data.Set("cert_type", "1") + data.Set("privateKey", keyPem) + data.Set("certPem", certPem) + _, err := RequestBt(&data, "POST", providerID, "/config?action=SetPanelSSL") + if err != nil { + return fmt.Errorf("证书部署失败: %v", err) + } + return nil +} + +func DeployBtSite(cfg map[string]any) error { + cert, ok := cfg["certificate"].(map[string]any) + if !ok { + return fmt.Errorf("证书不存在") + } + // 设置证书 + keyPem, ok := cert["key"].(string) + if !ok { + return fmt.Errorf("证书错误:key") + } + certPem, ok := cert["cert"].(string) + if !ok { + return fmt.Errorf("证书错误:cert") + } + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return fmt.Errorf("参数错误:provider_id") + } + siteName, ok := cfg["siteName"].(string) + if !ok { + return fmt.Errorf("参数错误:siteName") + } + data := url.Values{} + data.Set("key", keyPem) + data.Set("csr", certPem) + data.Set("siteName", siteName) + _, err := RequestBt(&data, "POST", providerID, "/site?action=SetSSL") + if err != nil { + return fmt.Errorf("证书部署失败: %v", err) + } + return nil +} diff --git a/backend/internal/cert/deploy/deploy.go b/backend/internal/cert/deploy/deploy.go new file mode 100644 index 0000000..8487d24 --- /dev/null +++ b/backend/internal/cert/deploy/deploy.go @@ -0,0 +1,45 @@ +package deploy + +import ( + "ALLinSSL/backend/public" + "fmt" +) + +func Deploy(cfg map[string]any, logger *public.Logger) error { + providerName, ok := cfg["provider"].(string) + if !ok { + return fmt.Errorf("provider is not string") + } + switch providerName { + case "btpanel": + logger.Debug("部署到宝塔面板...") + return DeployBt(cfg) + case "btpanel-site": + logger.Debug("部署到宝塔面板网站...") + return DeployBtSite(cfg) + case "tencentcloud-cdn": + cfg["resource_type"] = "cdn" + logger.Debug("部署到腾讯云CDN...") + return DeployToTX(cfg) + case "tencentcloud-cos": + cfg["resource_type"] = "cos" + logger.Debug("部署到腾讯云COS...") + return DeployToTX(cfg) + case "1panel": + logger.Debug("部署到1Panel...") + return Deploy1panel(cfg) + case "1panel-site": + logger.Debug("部署到1Panel网站...") + return Deploy1panelSite(cfg) + case "ssh": + logger.Debug("使用ssh部署到指定路径...") + return DeploySSH(cfg) + case "aliyun-cdn": + logger.Debug("部署到阿里云CDN...") + return DeployAliCdn(cfg) + // case "aliyun-oss": + + default: + return fmt.Errorf("不支持的部署: %s", providerName) + } +} diff --git a/backend/internal/cert/deploy/ssh.go b/backend/internal/cert/deploy/ssh.go new file mode 100644 index 0000000..93f1b90 --- /dev/null +++ b/backend/internal/cert/deploy/ssh.go @@ -0,0 +1,163 @@ +package deploy + +import ( + "ALLinSSL/backend/internal/access" + "bytes" + "encoding/json" + "fmt" + "golang.org/x/crypto/ssh" + "path" + "strconv" +) + +type SSHConfig struct { + User string + Password string // 可选 + PrivateKey string // 可选 + Host string + Port string +} + +type RemoteFile struct { + Path string + Content string +} + +func buildAuthMethods(password, privateKey string) ([]ssh.AuthMethod, error) { + var methods []ssh.AuthMethod + + if privateKey != "" { + signer, err := ssh.ParsePrivateKey([]byte(privateKey)) + if err != nil { + return nil, fmt.Errorf("unable to parse private key: %v", err) + } + methods = append(methods, ssh.PublicKeys(signer)) + } + + if password != "" { + methods = append(methods, ssh.Password(password)) + } + + if len(methods) == 0 { + return nil, fmt.Errorf("no authentication methods provided") + } + + return methods, nil +} + +func writeMultipleFilesViaSSH(config SSHConfig, files []RemoteFile, preCmd, postCmd string) error { + addr := fmt.Sprintf("%s:%s", config.Host, config.Port) + + authMethods, err := buildAuthMethods(config.Password, config.PrivateKey) + if err != nil { + return err + } + + sshConfig := &ssh.ClientConfig{ + User: config.User, + Auth: authMethods, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + client, err := ssh.Dial("tcp", addr, sshConfig) + if err != nil { + return fmt.Errorf("failed to dial: %v", err) + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + return fmt.Errorf("会话创建失败: %v", err) + } + defer session.Close() + + var script bytes.Buffer + + if preCmd != "" { + script.WriteString(preCmd + " && ") + } + + for i, file := range files { + if i > 0 { + script.WriteString(" && ") + } + + dirCmd := fmt.Sprintf("mkdir -p $(dirname %q)", file.Path) + writeCmd := fmt.Sprintf("printf %%s '%s' > %s", file.Content, file.Path) + + script.WriteString(dirCmd + " && " + writeCmd) + } + + if postCmd != "" { + script.WriteString(" && " + postCmd) + } + + cmd := script.String() + + if err := session.Run(cmd); err != nil { + return fmt.Errorf("运行出错: %v", err) + } + + return nil +} + +func DeploySSH(cfg map[string]any) error { + cert, ok := cfg["certificate"].(map[string]any) + if !ok { + return fmt.Errorf("证书不存在") + } + // 设置证书 + keyPem, ok := cert["key"].(string) + if !ok { + return fmt.Errorf("证书错误:key") + } + certPem, ok := cert["cert"].(string) + if !ok { + return fmt.Errorf("证书错误:cert") + } + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return fmt.Errorf("参数错误:provider_id") + } + dir, ok := cfg["path"].(string) + if !ok { + return fmt.Errorf("参数错误:path") + } + beforeCmd, ok := cfg["beforeCmd"].(string) + if !ok { + return fmt.Errorf("参数错误:beforeCmd") + } + afterCmd, ok := cfg["afterCmd"].(string) + if !ok { + return fmt.Errorf("参数错误:afterCmd") + } + providerData, err := access.GetAccess(providerID) + if err != nil { + return err + } + providerConfigStr, ok := providerData["config"].(string) + if !ok { + return fmt.Errorf("api配置错误") + } + // 解析 JSON 配置 + var providerConfig SSHConfig + err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) + if err != nil { + return err + } + // 自动创建多级目录 + files := []RemoteFile{ + {Path: path.Join(dir, "cert.pem"), Content: certPem}, + {Path: path.Join(dir, "key.pem"), Content: keyPem}, + } + err = writeMultipleFilesViaSSH(providerConfig, files, beforeCmd, afterCmd) + if err != nil { + return fmt.Errorf("SSH 部署失败: %v", err) + } + return nil +} diff --git a/backend/internal/cert/deploy/ssh_test.go b/backend/internal/cert/deploy/ssh_test.go new file mode 100644 index 0000000..ecdde4f --- /dev/null +++ b/backend/internal/cert/deploy/ssh_test.go @@ -0,0 +1,22 @@ +package deploy + +import "testing" + +func TestSSH(t *testing.T) { + cfg := map[string]any{ + "path": "/www/ccccc", + "beforeCmd": "touch /www/ccccc/xxxxx.txt", + "afterCmd": "touch /www/ccccc/cccccc.txt", + "provider_id": "23", + "certificate": map[string]any{ + "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxIjmAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONh\nWhMT6W+cx0WMC80yCRm5JshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM\n4tz/Zh8a3kVyN4MtWDmV1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1\nz6qqEDcM8FtHoAXAdxQBkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9\ncMY1cCEBxpwQTJiJHbX9LcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MU\nS89+DsixFf3HL+iWjr6yVnQ/mAGVPQ+HD4pwmQIDAQABAoIBAALpcFb59MBZZHJ3\nui9RRi96ig6kPQoRjkjN83pjM+/h/bANMmUOQU5FHBKLwj5uhN5Dpk2fzAnIX2TE\nVgfyNGsYuWLsIM+m6EJfm7pXJwJDr3RCpm+6DIKr1U8TwlR2OhbDi6fOlfH66q79\n2Klq4SXsa0vgfllpTVCDtydFVjwAuQV7Cf6DGRjbNpN3DPLeOC1wYFimNZwudSK0\nf8grWpPFXw2TPaf3TgeBGxwL7GCTYSKT+Eq9USbhG4RArrM9oQt+h7rzaH2bFEdg\n7tOM4KIgV+aw8r0TsYisDG9dfiHfHr5vQnkmWgt/rxAOvHlJ7/64pBVuET1ZF0mB\nP6gu4Y0CgYEAzkwXvfnHI5qx9BVP6e9lGrpWrm0RxCKr2iCCwrOVALbX1yfKCb5L\nrP/jSERMuLt6bIKg/AoVu9ogCTGzntyHTbZXFGg/y5Xoul+1af2arQ1rGZ7A/Im7\nnteZePg2U6UiDRy07F94FF5aL/v97D4BffiSA+0atlgH6tpKyYfY6NsCgYEA8+Ku\nGQqX9kHDd5bbzPhLelNmHVnAjnMaHEhvzVtBA737F10Oqg9wyffqe/i/DvdUSx9r\nafKGUfzB2vVZjz//OpSQ8VhRzDTiyelKLsSTmzOokLBnwayyTxw85o9EDvTNrzfb\nYQbAjmAXWmnv5Xvx1KfvTaKFY3BmHsKYJDzwnJsCgYBK1SVjn2CSVMIqlTSI2nMl\nb+STnzLrn9wQ4uwr7nKlcK34+RD72dCfr67lfwkJldBB3lzBMHNT0jr+us26Waqn\nEPaji3Fgyz9BpAgtq3XZQl3QTFsbAGdTpkegrwEd9G/Wq8whVjw7v0Id193zPUbT\nSEDHNdITxPkSQx8P3bxcMwKBgQDO5EGk5KO9OFTFoqib3RbKku1RgM4lCefgjmKp\n5vvkXMohK8RA6BBahYHZ4U7TN2W+xMyueBsSekVJplFvgG7YFyhOVQovHb42Yz2X\nJxPA2bXp6HxchFBPZDkVrfuiZHIIbm4ghUXcgg/Nl4j3OIoSSNRtG63kiXlYJuRB\n+aB0eQKBgD79VrREpbOMS7HRlDTtfkDN94HY3T4MLErs26z/NLO/dC44tmBJGo2P\ngcQ+p7XxNjpWUnUbEiuz4R3Xgh6ULwuSseWtcQicolPHTkBjnc+6BEpyguZJ+FPZ\nGls3g3LxjGhdPlyd37CaWDvx/Jtjrd4Y9iGkGO2d9fXZD0Hg0ymX\n-----END RSA PRIVATE KEY-----", + "cert": "-----BEGIN CERTIFICATE-----\nMIIG5DCCBMygAwIBAgIQBPQGlt81+4RKt3RAFXPvrjANBgkqhkiG9w0BAQsFADBb\nMQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywg\nSW5jLjElMCMGA1UEAxMcVHJ1c3RBc2lhIERWIFRMUyBSU0EgQ0EgMjAyNTAeFw0y\nNTA0MjIwMDAwMDBaFw0yNTA3MjAyMzU5NTlaMB8xHTAbBgNVBAMTFGFsbGluc3Ns\nLnphY2h5YW5nLmNuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxIjm\nAi/paC2OmG7nOqZ+OJx7spDrx7yZiWvn1XgLW/5ODONhWhMT6W+cx0WMC80yCRm5\nJshIIMzmMxN03pRD1h4u1fPNUnJmGtthRZIm3aU7TlSM4tz/Zh8a3kVyN4MtWDmV\n1/1MV8H0YBtT6K2gxZ7Fz/YKhVATdh8Fy+1qEz3gSrw1z6qqEDcM8FtHoAXAdxQB\nkS8xu34SIriwZiN2YlrtL8Qy73j4XiJLh2cc/NPp+mW9cMY1cCEBxpwQTJiJHbX9\nLcEqYgOkkhWIijW2dYlCLaLsnvJw0TCRd6PooR8XK7MUS89+DsixFf3HL+iWjr6y\nVnQ/mAGVPQ+HD4pwmQIDAQABo4IC3jCCAtowHwYDVR0jBBgwFoAUtBIopbTAHZ8p\ncWk82RGWSnVpUMAwHQYDVR0OBBYEFHqqdlMVBlcadf7iJLJoLnLZ7h4tMB8GA1Ud\nEQQYMBaCFGFsbGluc3NsLnphY2h5YW5nLmNuMD4GA1UdIAQ3MDUwMwYGZ4EMAQIB\nMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNV\nHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHkGCCsG\nAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t\nMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vVHJ1c3RB\nc2lhRFZUTFNSU0FDQTIwMjUuY3J0MAwGA1UdEwEB/wQCMAAwggF9BgorBgEEAdZ5\nAgQCBIIBbQSCAWkBZwB2ABLxTjS9U3JMhAYZw48/ehP457Vih4icbTAFhOvlhiY6\nAAABll0w/o0AAAQDAEcwRQIgd24jCPm+fbHq3grMIxtvQhzkv7dvYPM/BGjPEsy1\nQ70CIQC5jXADjBh+dH50T+atn3lktBEqQhedOl6cAaP/XXmk6gB2AO08S9boBsKk\nogBX28sk4jgB31Ev7cSGxXAPIN23Pj/gAAABll0w/rUAAAQDAEcwRQIgU2GDVEH1\ns5i/RC1RhqvJjn72PAZOlDtJyLdg29vC9HECIQCj78GATYK5quitLxbn3HvD8BeT\noOz+3tacgyN6+TdvugB1AKRCxQZJYGFUjw/U6pz7ei0mRU2HqX8v30VZ9idPOoRU\nAAABll0w/sYAAAQDAEYwRAIgCvU/iBRPKoJLjmU4edBYObWAO/aJp2mWnfJ4ieAr\nrXsCIBsAppYu28h8YEOl0N9yEeF9G05IMxwkCjZKonQs2SKMMA0GCSqGSIb3DQEB\nCwUAA4ICAQB3wFou51Qvl4apMhencuQUnWF3UpYP49e0WQ72DVT3pYjYsozkSuqb\nQZcwMB6HDoHdFicxvQ/yxKyTu/nw3rXjUWYuSxXYd7lJcQ/R0tR00m6AFeinY4Aq\nq4QqoA+lriK1XqO5MomAL4FbSysT1ow/gaG9pYuXEdT4pr05I/NumjXdkwBRZOd4\nrhol2grKf3y37Qla5hUbbG3ab9nf/csJSWkCoESeXr3MB1oAU/aL9pGSagvMXSKQ\nsFs2cn2Fi8ZmJPJXIP114lgvFuFDO+C1yTNbHap/FufvAKGryfPDuPecCF6FSXej\n+bwg4/BNz5lcHbNo2XXjLgoPg4VE6mG/SQQZQEDBk5DowwMVMvh77t9RBNrHozah\nHGtQz2hCuIX7rZQYnSlvW8T75FhI/Sd+HEfU/iyTIELXBUjypnK2bOJL7+jE7f79\nuljhXlCcP52fGHCjexNBz5gIZr82KVxsfxKuZjfioPkhmWleVNMdMWYJRXu618E6\nNtNjUVsDCuMOOMNs1qScqxOT60MeDZLX+vnC93fdd/t2hLEAWWNNMkWeX2qLCE1q\nGarop9U1mJpiBWkW5cBiqnNIbhuV2fcwFIR8mVT5f1Qcw+WxE2nEjY2h75bKv8T5\n3RBngmaX8PcyLAP2s0/4UyzAnMYfioJBh37VpUYBrdriBkRds/AMZw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFnjCCBIagAwIBAgIQCSYyO0lk42hGFRLe8aXVLDANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0yNTAxMDgwMDAwMDBaFw0zNTAxMDcyMzU5NTlaMFsxCzAJBgNVBAYTAkNO\nMSUwIwYDVQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSUwIwYDVQQD\nExxUcnVzdEFzaWEgRFYgVExTIFJTQSBDQSAyMDI1MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA0fuEmuBIsN6ZZVq+gRobMorOGIilTCIfQrxNpR8FUZ9R\n/GfbiekbiIKphQXEZ7N1uBnn6tXUuZ32zl6jPkZpHzN/Bmgk1BWSIzVc0npMzrWq\n/hrbk5+KddXJdsNpeG1+Q8lc8uVMBrztnxaPb7Rh7yQCsMrcO4hgVaqLJWkVvEfW\nULtoCHQnNaj4IroG6VxQf1oArQ8bPbwpI02lieSahRa78FQuXdoGVeQcrkhtVjZs\nON98vq5fPWZX2LFv7e5J6P9IHbzvOl8yyQjv+2/IOwhNSkaXX3bI+//bqF9XW/p7\n+gsUmHiK5YsvLjmXcvDmoDEGrXMzgX31Zl2nJ+umpRbLjwP8rxYIUsKoEwEdFoto\nAid59UEBJyw/GibwXQ5xTyKD/N6C8SFkr1+myOo4oe1UB+YgvRu6qSxIABo5kYdX\nFodLP4IgoVJdeUFs1Usa6bxYEO6EgMf5lCWt9hGZszvXYZwvyZGq3ogNXM7eKyi2\n20WzJXYMmi9TYFq2Fa95aZe4wki6YhDhhOO1g0sjITGVaB73G+JOCI9yJhv6+REN\nD40ZpboUHE8JNgMVWbG1isAMVCXqiADgXtuC+tmJWPEH9cR6OuJLEpwOzPfgAbnn\n2MRu7Tsdr8jPjTPbD0FxblX1ydW3RG30vwLF5lkTTRkHG9epMgpPMdYP7nY/08MC\nAwEAAaOCAVYwggFSMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLQSKKW0\nwB2fKXFpPNkRlkp1aVDAMB8GA1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485\nMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\ndgYIKwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy\ndC5jb20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E\naWdpQ2VydEdsb2JhbFJvb3RHMi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov\nL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDARBgNV\nHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQELBQADggEBAJ4a3svh316GY2+Z7EYx\nmBIsOwjJSnyoEfzx2T699ctLLrvuzS79Mg3pPjxSLlUgyM8UzrFc5tgVU3dZ1sFQ\nI4RM+ysJdvIAX/7Yx1QbooVdKhkdi9X7QN7yVkjqwM3fY3WfQkRTzhIkM7mYIQbR\nr+y2Vkju61BLqh7OCRpPMiudjEpP1kEtRyGs2g0aQpEIqKBzxgitCXSayO1hoO6/\n71ts801OzYlqYW9OQQQ2GCJyFbD6XHDjdpn+bWUxTKWaMY0qedSCbHE3Kl2QEF0C\nynZ7SbC03yR+gKZQDeTXrNP1kk5Qhe7jSXgw+nhbspe0q/M1ZcNCz+sPxeOwdCcC\ngJE=\n-----END CERTIFICATE-----", + "issuer": "cert-issuer", + }, + } + err := DeploySSH(cfg) + if err != nil { + t.Fatalf("DeploySSH failed: %v", err) + } + // println(err.Error()) +} diff --git a/backend/internal/cert/deploy/tencentcloud.go b/backend/internal/cert/deploy/tencentcloud.go new file mode 100644 index 0000000..c09f526 --- /dev/null +++ b/backend/internal/cert/deploy/tencentcloud.go @@ -0,0 +1,134 @@ +package deploy + +import ( + "ALLinSSL/backend/internal/access" + "encoding/json" + "fmt" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + "strconv" + "strings" +) + +func ClientTencentcloud(SecretId, SecretKey, region string) *ssl.Client { + credential := common.NewCredential( + SecretId, + SecretKey, + ) + // 实例化一个client选项,可选的,没有特殊需求可以跳过 + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" + // 实例化要请求产品的client对象,clientProfile是可选的 + client, _ := ssl.NewClient(credential, region, cpf) + return client +} + +func UploadToTX(client *ssl.Client, key, cert string) (string, error) { + request := ssl.NewUploadCertificateRequest() + request.CertificatePublicKey = common.StringPtr(cert) + request.CertificatePrivateKey = common.StringPtr(key) + // 返回的resp是一个UploadCertificateResponse的实例,与请求对象对应 + response, err := client.UploadCertificate(request) + if _, ok := err.(*errors.TencentCloudSDKError); ok { + return "", err + } + if err != nil { + return "", err + } + return *response.Response.CertificateId, nil +} + +func DeployToTX(cfg map[string]any) error { + cert, ok := cfg["certificate"].(map[string]any) + if !ok { + return fmt.Errorf("证书不存在") + } + keyPem, ok := cert["key"].(string) + if !ok { + return fmt.Errorf("证书错误:key") + } + certPem, ok := cert["cert"].(string) + if !ok { + return fmt.Errorf("证书错误:cert") + } + + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return fmt.Errorf("参数错误:provider_id") + } + // + providerData, err := access.GetAccess(providerID) + if err != nil { + return err + } + providerConfigStr, ok := providerData["config"].(string) + if !ok { + return fmt.Errorf("api配置错误") + } + // 解析 JSON 配置 + var providerConfig map[string]string + err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) + if err != nil { + return err + } + region := "" + if r, ok := cfg["region"].(string); ok { + region = r + } + client := ClientTencentcloud(providerConfig["secret_id"], providerConfig["secret_key"], region) + + // 上传证书 + certificateId, err := UploadToTX(client, strings.TrimSpace(keyPem), strings.TrimSpace(certPem)) + if err != nil { + return err + } + // fmt.Println(certificateId) + + request := ssl.NewDeployCertificateInstanceRequest() + + request.CertificateId = common.StringPtr(certificateId) + if cfg["resource_type"] == "cdn" { + domain, ok := cfg["domain"].(string) + if !ok { + return fmt.Errorf("参数错误:domain") + } + request.InstanceIdList = common.StringPtrs([]string{domain}) + request.ResourceType = common.StringPtr("cdn") + } + if cfg["resource_type"] == "cos" { + // fmt.Println(fmt.Sprintf("%s|%s|%s", cfg["region"].(string), cfg["bucket"].(string), cfg["domain"].(string))) + domain, ok := cfg["domain"].(string) + if !ok { + return fmt.Errorf("参数错误:domain") + } + region, ok := cfg["region"].(string) + if !ok { + return fmt.Errorf("参数错误:region") + } + bucket, ok := cfg["domain"].(string) + if !ok { + return fmt.Errorf("参数错误:bucket") + } + request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", region, bucket, domain)}) + // request.InstanceIdList = common.StringPtrs([]string{"ap-guangzhou#allinssl-1253163109#allinssl.zachyang.cn"}) + request.ResourceType = common.StringPtr("cos") + } + + // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 + response, err := client.DeployCertificateInstance(request) + if _, ok := err.(*errors.TencentCloudSDKError); ok { + return err + } + if err != nil { + panic(err) + } + fmt.Println(response.Response.DeployRecordId) + return nil +} diff --git a/backend/internal/overview/overview.go b/backend/internal/overview/overview.go new file mode 100644 index 0000000..36bf68c --- /dev/null +++ b/backend/internal/overview/overview.go @@ -0,0 +1,163 @@ +package overview + +import ( + "ALLinSSL/backend/internal/cert" + "ALLinSSL/backend/internal/workflow" + "ALLinSSL/backend/public" + "time" +) + +func GetWorkflowCount() (map[string]any, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + defer s.Close() + workflow, err := s.Query(`select count(*) as count, + count(case when active=1 then 1 end ) as active, + count(case when last_run_status='fail' then 1 end ) as failure + from workflow +`) + if err != nil { + return nil, err + } + if len(workflow) == 0 { + return nil, nil + } + return workflow[0], err +} + +func GetCertCount() (map[string]int, error) { + s, err := cert.GetSqlite() + if err != nil { + return nil, err + } + defer s.Close() + data, err := s.Select() + if err != nil { + return nil, err + } + if len(data) == 0 { + return nil, nil + } + result := map[string]int{ + "count": len(data), + "will": 0, + "end": 0, + } + for _, v := range data { + endTimeStr, ok := v["end_time"].(string) + if !ok { + continue + } + endTime, err := time.Parse("2006-01-02 15:04:05", endTimeStr) + if err != nil { + continue + } + if endTime.Before(time.Now()) { + result["end"]++ + } else { + if endTime.Sub(time.Now()).Hours() < 24*30 { + result["will"]++ + } + } + } + return result, nil +} + +func GetSiteMonitorCount() (map[string]any, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + defer s.Close() + cert, err := s.Query(`select count(*) as count, + count(case when state='异常' then 1 end ) as exception + from site_monitor`) + if err != nil { + return nil, err + } + if len(cert) == 0 { + return nil, nil + } + return cert[0], nil +} + +func GetWorkflowHistory() ([]map[string]any, error) { + s, err := workflow.GetSqliteObjWH() + if err != nil { + return nil, err + } + defer s.Close() + data, err := s.Limit([]int64{0, 3}).Order("create_time", "desc").Select() + if err != nil { + return nil, err + } + s.TableName = "workflow" + var result []map[string]any + for _, v := range data { + var ( + mode string + name string + state int + ) + switch v["status"] { + case "success": + state = 1 + case "fail": + state = -1 + case "running": + state = 0 + } + switch v["exec_type"] { + case "manual": + mode = "手动触发" + case "auto": + mode = "定时触发" + } + wk, err := s.Where("id=?", []interface{}{v["workflow_id"]}).Select() + if err != nil { + continue + } + if len(wk) > 0 { + name = wk[0]["name"].(string) + } else { + name = "未知" + } + + result = append(result, map[string]any{ + "name": name, + "state": state, + "mode": mode, + "exec_time": v["create_time"], + }) + } + return result, nil +} + +func GetOverviewData() (map[string]any, error) { + workflowCount, err := GetWorkflowCount() + if err != nil { + return nil, err + } + certCount, err := GetCertCount() + if err != nil { + return nil, err + } + siteMonitorCount, err := GetSiteMonitorCount() + if err != nil { + return nil, err + } + workflowHistory, err := GetWorkflowHistory() + if err != nil { + return nil, err + } + result := make(map[string]any) + result["workflow"] = workflowCount + result["cert"] = certCount + result["site_monitor"] = siteMonitorCount + result["workflow_history"] = workflowHistory + return result, nil +} diff --git a/backend/internal/report/report.go b/backend/internal/report/report.go new file mode 100644 index 0000000..c23b76d --- /dev/null +++ b/backend/internal/report/report.go @@ -0,0 +1,189 @@ +package report + +import ( + "ALLinSSL/backend/public" + "crypto/tls" + "encoding/json" + "fmt" + "github.com/jordan-wright/email" + "net/smtp" + "time" +) + +func GetSqlite() (*public.Sqlite, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + s.TableName = "report" + return s, nil +} + +func GetList(search string, p, limit int64) ([]map[string]any, int, error) { + var data []map[string]any + var count int64 + s, err := GetSqlite() + if err != nil { + return data, 0, err + } + defer s.Close() + + var limits []int64 + if p >= 0 && limit >= 0 { + limits = []int64{0, limit} + if p > 1 { + limits[0] = (p - 1) * limit + limits[1] = p * limit + } + } + + if search != "" { + count, err = s.Where("name like ?", []interface{}{"%" + search + "%"}).Count() + data, err = s.Where("name like ?", []interface{}{"%" + search + "%"}).Limit(limits).Order("update_time", "desc").Select() + } else { + count, err = s.Count() + data, err = s.Order("update_time", "desc").Limit(limits).Select() + } + if err != nil { + return data, 0, err + } + return data, int(count), nil +} + +func GetReport(id string) (map[string]any, error) { + s, err := GetSqlite() + if err != nil { + return nil, err + } + defer s.Close() + data, err := s.Where("id=?", []interface{}{id}).Select() + if err != nil { + return nil, err + } + if len(data) == 0 { + return nil, fmt.Errorf("没有找到此通知配置") + } + return data[0], nil + +} + +func AddReport(Type, config, name string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + now := time.Now().Format("2006-01-02 15:04:05") + _, err = s.Insert(map[string]interface{}{ + "name": name, + "type": Type, + "config": config, + "create_time": now, + "update_time": now, + }) + return err +} + +func UpdReport(id, config, name string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + _, err = s.Where("id=?", []interface{}{id}).Update(map[string]interface{}{ + "name": name, + "config": config, + }) + return err +} + +func DelReport(id string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + _, err = s.Where("id=?", []interface{}{id}).Delete() + return err +} + +func NotifyTest(id string) error { + if id == "" { + return fmt.Errorf("缺少参数") + } + providerData, err := GetReport(id) + if err != nil { + return err + } + params := map[string]any{ + "provider_id": id, + "body": "测试消息通道", + "subject": "测试消息通道", + } + switch providerData["type"] { + case "mail": + err = NotifyMail(params) + } + return err +} + +func Notify(params map[string]any) error { + if params == nil { + return fmt.Errorf("缺少参数") + } + providerName, ok := params["provider"].(string) + if !ok { + return fmt.Errorf("通知类型错误") + } + switch providerName { + case "mail": + return NotifyMail(params) + // case "btpanel-site": + // return NotifyBt(params) + default: + return fmt.Errorf("不支持的通知类型") + } +} + +func NotifyMail(params map[string]any) error { + + if params == nil { + return fmt.Errorf("缺少参数") + } + providerID := params["provider_id"].(string) + // fmt.Println(providerID) + providerData, err := GetReport(providerID) + if err != nil { + return err + } + configStr := providerData["config"].(string) + var config map[string]string + err = json.Unmarshal([]byte(configStr), &config) + if err != nil { + return fmt.Errorf("解析配置失败: %v", err) + } + + e := email.NewEmail() + e.From = config["sender"] + e.To = []string{config["receiver"]} + e.Subject = params["subject"].(string) + + e.Text = []byte(params["body"].(string)) + + addr := fmt.Sprintf("%s:%s", config["smtpHost"], config["smtpPort"]) + + auth := smtp.PlainAuth("", config["sender"], config["password"], config["smtpHost"]) + + // 使用 SSL(通常是 465) + if config["smtpPort"] == "465" { + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, // 开发阶段跳过证书验证,生产建议关闭 + ServerName: config["smtpHost"], + } + return e.SendWithTLS(addr, auth, tlsConfig) + } + + // 普通明文发送(25端口,非推荐) + return e.Send(addr, auth) +} diff --git a/backend/internal/report/report_test.go b/backend/internal/report/report_test.go new file mode 100644 index 0000000..64dd23e --- /dev/null +++ b/backend/internal/report/report_test.go @@ -0,0 +1,17 @@ +package report + +import ( + "fmt" + "testing" +) + +func TestMail(t *testing.T) { + config := map[string]any{ + "provider": "mail", + "provider_id": "4", + "subject": "执行结束", + "body": "执行结束", + } + err := NotifyMail(config) + fmt.Println(err) +} diff --git a/backend/internal/setting/setting.go b/backend/internal/setting/setting.go new file mode 100644 index 0000000..9d2b865 --- /dev/null +++ b/backend/internal/setting/setting.go @@ -0,0 +1,182 @@ +package setting + +import ( + "ALLinSSL/backend/public" + "crypto/md5" + "encoding/hex" + "fmt" + "github.com/joho/godotenv" + "os" + "strconv" + "syscall" + "time" +) + +type Setting struct { + Timeout int `json:"timeout" form:"timeout"` + Secure string `json:"secure" form:"secure"` + Https string `json:"https" form:"https"` + Key string `json:"key" form:"key"` + Cert string `json:"cert" form:"cert"` + Username string `json:"username" form:"username"` + Password string `json:"password" form:"password"` +} + +func Get() (Setting, error) { + var setting = Setting{ + Timeout: public.TimeOut, + Secure: public.Secure, + } + + setting.Https = public.GetSettingIgnoreError("https") + key, err := os.ReadFile("data/https/key.pem") + if err != nil { + key = []byte{} + } + cert, err := os.ReadFile("data/https/cert.pem") + if err != nil { + cert = []byte{} + } + setting.Key = string(key) + setting.Cert = string(cert) + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return setting, err + } + defer s.Close() + s.TableName = "users" + data, err := s.Select() + if err != nil { + return setting, err + } + if len(data) == 0 { + return setting, fmt.Errorf("no users found") + } + username := data[0]["username"].(string) + setting.Username = username + return setting, nil +} + +func Save(setting *Setting) error { + var restart bool + var reload bool + + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return err + } + defer s.Close() + if setting.Username != "" || setting.Password != "" { + s.TableName = "users" + user, err := s.Where("id=1", []interface{}{}).Select() + if err != nil { + return err + } + if len(user) == 0 { + return fmt.Errorf("no users found") + } + data := map[string]interface{}{} + if setting.Username != "" { + data["username"] = setting.Username + } + + salt := user[0]["salt"].(string) + passwd := setting.Password + salt + // fmt.Println(passwd) + keyMd5 := md5.Sum([]byte(passwd)) + passwdMd5 := hex.EncodeToString(keyMd5[:]) + if setting.Password != "" { + data["password"] = passwdMd5 + } + _, err = s.Where("id=1", []interface{}{}).Update(data) + if err != nil { + return err + } + reload = true + } + s.TableName = "settings" + if setting.Timeout != 0 { + s.Where("key = 'timeout'", []interface{}{}).Update(map[string]interface{}{"value": setting.Timeout}) + public.TimeOut = setting.Timeout + } + if setting.Secure != "" { + s.Where("key = 'secure'", []interface{}{}).Update(map[string]interface{}{"value": setting.Secure}) + public.TimeOut = setting.Timeout + } + if setting.Https == "1" { + if setting.Key == "" || setting.Cert == "" { + return fmt.Errorf("key or cert is empty") + } + // fmt.Println(setting.Key, setting.Cert) + err := public.ValidateSSLCertificate(setting.Cert, setting.Key) + if err != nil { + return err + } + s.Where("key = 'https'", []interface{}{}).Update(map[string]interface{}{"value": setting.Https}) + // dir := filepath.Dir("data/https") + if err := os.MkdirAll("data/https", os.ModePerm); err != nil { + panic("创建目录失败: " + err.Error()) + } + err = os.WriteFile("data/https/key.pem", []byte(setting.Key), 0644) + // fmt.Println(err) + os.WriteFile("data/https/cert.pem", []byte(setting.Cert), 0644) + restart = true + } + + if restart { + Restart() + return nil + } else { + if reload { + s.Where("key = 'login_key'", []interface{}{}).Update(map[string]interface{}{"value": public.GenerateUUID()}) + } + } + return nil +} + +func Shutdown() { + go func() { + time.Sleep(time.Millisecond * 100) + public.ShutdownFunc() + }() + return +} + +func Restart() { + go func() { + time.Sleep(time.Millisecond * 100) + env, err := godotenv.Read("data/.env") + if err != nil { + env = map[string]string{ + "web": "restart", + "scheduler": "start", + } + } + pidStr, err := os.ReadFile("data/pid") + if err != nil { + fmt.Println("Error reading pid file") + return + } + err = godotenv.Write(env, "data/.env") + if err != nil { + fmt.Println("Error writing to .env file") + return + } + pid, err := strconv.Atoi(string(pidStr)) + if err != nil { + fmt.Println("Error converting pid to int:", err) + return + } + process, err := os.FindProcess(pid) + if err != nil { + fmt.Println("Error finding process:", err) + return + } + err = process.Signal(syscall.SIGHUP) + if err != nil { + fmt.Println("Error sending signal:", err) + return + } + }() + return +} diff --git a/backend/internal/siteMonitor/monitor.go b/backend/internal/siteMonitor/monitor.go new file mode 100644 index 0000000..2ee8124 --- /dev/null +++ b/backend/internal/siteMonitor/monitor.go @@ -0,0 +1,249 @@ +package siteMonitor + +import ( + "ALLinSSL/backend/public" + "crypto/tls" + "fmt" + "net" + "net/http" + "strings" + "time" +) + +// SSLInfo 定义结果结构体 +type SSLInfo struct { + Target string + HTTPStatus int + HTTPStatusText string + Domains []string + Issuer string + NotBefore string + NotAfter string + DaysRemaining int + CertificateOK bool + CertificateNote string +} + +func GetSqlite() (*public.Sqlite, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + s.TableName = "site_monitor" + return s, nil +} + +func GetList(search string, p, limit int64) ([]map[string]any, int, error) { + var data []map[string]any + var count int64 + s, err := GetSqlite() + if err != nil { + return data, 0, err + } + defer s.Close() + + var limits []int64 + if p >= 0 && limit >= 0 { + limits = []int64{0, limit} + if p > 1 { + limits[0] = (p - 1) * limit + limits[1] = p * limit + } + } + + if search != "" { + count, err = s.Where("name like ? or site_domain like ?", []interface{}{"%" + search + "%", "%" + search + "%"}).Count() + data, err = s.Where("name like ? or site_domain like ?", []interface{}{"%" + search + "%", "%" + search + "%"}).Order("update_time", "desc").Limit(limits).Select() + } else { + count, err = s.Count() + data, err = s.Order("update_time", "desc").Limit(limits).Select() + } + if err != nil { + return data, 0, err + } + for _, v := range data { + v["domain"] = v["site_domain"] + } + + return data, int(count), nil +} + +func AddMonitor(name, domain, reportType string, cycle int) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + info, err := CheckWebsite(domain) + if err != nil { + return err + } + _, err = s.Insert(map[string]any{ + "name": name, + "site_domain": domain, + "report_type": reportType, + "cycle": cycle, + "state": info.HTTPStatusText, + "ca": info.Issuer, + "cert_domain": strings.Join(info.Domains, ","), + "end_time": info.NotAfter, + "end_day": info.DaysRemaining, + "create_time": time.Now().Format("2006-01-02 15:04:05"), + "update_time": time.Now().Format("2006-01-02 15:04:05"), + "last_time": time.Now().Format("2006-01-02 15:04:05"), + "active": 1, + }) + if err != nil { + return err + } + return nil +} + +func UpdMonitor(id, name, domain, reportType string, cycle int) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + + info, err := CheckWebsite(domain) + if err != nil { + return err + } + _, err = s.Where("id=?", []interface{}{id}).Update(map[string]any{ + "name": name, + "site_domain": domain, + "report_type": reportType, + "cycle": cycle, + "state": info.HTTPStatusText, + "ca": info.Issuer, + "cert_domain": strings.Join(info.Domains, ","), + "end_time": info.NotAfter, + "end_day": info.DaysRemaining, + "update_time": time.Now().Format("2006-01-02 15:04:05"), + "last_time": time.Now().Format("2006-01-02 15:04:05"), + "active": 1, + }) + if err != nil { + return err + } + return nil +} + +func DelMonitor(id string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + _, err = s.Where("id=?", []interface{}{id}).Delete() + if err != nil { + return err + } + return nil +} + +func SetMonitor(id string, active int) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + _, err = s.Where("id=?", []interface{}{id}).Update(map[string]any{ + "active": active, + "update_time": time.Now().Format("2006-01-02 15:04:05"), + }) + if err != nil { + return err + } + return nil +} + +func UpdInfo(id, domain string, s *public.Sqlite, reportType string) error { + info, errCheck := CheckWebsite(domain) + now := time.Now() + updateData := map[string]any{ + "state": info.HTTPStatusText, + "ca": info.Issuer, + "cert_domain": strings.Join(info.Domains, ","), + "end_time": info.NotAfter, + "end_day": info.DaysRemaining, + "last_time": now.Format("2006-01-02 15:04:05"), + "except_end_time": now.Format("2006-01-02 15:04:05"), + } + if errCheck != nil { + updateData["state"] = "异常" + // return err + } else { + if info.HTTPStatus != 0 && info.CertificateOK != false { + delete(updateData, "except_end_time") + } else { + errCheck = fmt.Errorf("证书异常") + } + } + _, err := s.Where("id=?", []interface{}{id}).Update(updateData) + if err != nil { + return err + } + return errCheck +} + +// CheckWebsite 实际检测函数 +func CheckWebsite(target string) (*SSLInfo, error) { + result := &SSLInfo{Target: target} + + // 验证格式是否是 IP 或域名 + if net.ParseIP(target) == nil { + if _, err := net.LookupHost(target); err != nil { + return result, fmt.Errorf("无效域名或 IP:%v", err) + } + } + + hostPort := net.JoinHostPort(target, "443") + + // result := &SSLInfo{Target: target} + + // 1. TLS 连接(先做,否则无 HTTPS 支持直接失败) + conn, err := tls.Dial("tcp", hostPort, &tls.Config{ + InsecureSkipVerify: true, + }) + if err != nil { + return result, fmt.Errorf("目标不支持 HTTPS:%v", err) + } + defer conn.Close() + + // 发送 HTTPS 请求检测状态 + resp, err := http.Get("https://" + target) + if err != nil { + result.HTTPStatus = 0 + result.HTTPStatusText = "异常" + } else { + result.HTTPStatus = resp.StatusCode + result.HTTPStatusText = "正常" + resp.Body.Close() + } + + // 获取证书 + cert := conn.ConnectionState().PeerCertificates[0] + result.Domains = cert.DNSNames + result.Issuer = cert.Issuer.CommonName + result.NotBefore = cert.NotBefore.Format("2006-01-02 15:04:05") + result.NotAfter = cert.NotAfter.Format("2006-01-02 15:04:05") + result.DaysRemaining = int(cert.NotAfter.Sub(time.Now()).Hours() / 24) + + now := time.Now() + switch { + case now.Before(cert.NotBefore): + result.CertificateOK = false + result.CertificateNote = "尚未生效" + case now.After(cert.NotAfter): + result.CertificateOK = false + result.CertificateNote = "已过期" + default: + result.CertificateOK = true + result.CertificateNote = "有效" + } + + return result, nil +} diff --git a/backend/internal/siteMonitor/monitor_test.go b/backend/internal/siteMonitor/monitor_test.go new file mode 100644 index 0000000..a360309 --- /dev/null +++ b/backend/internal/siteMonitor/monitor_test.go @@ -0,0 +1,21 @@ +package siteMonitor + +import ( + "fmt" + "testing" +) + +func Test(t *testing.T) { + site := "bt.cn" // 只传域名或 IP,不要 http:// + result, err := CheckWebsite(site) + if err != nil { + fmt.Printf("❌ 检测失败: %v\n", err) + return + } + fmt.Println(result.HTTPStatusText) + fmt.Println(result.Domains) + fmt.Println(result.Issuer) + fmt.Println(result.NotAfter) + // fmt.Println(result.Domains) + // fmt.Println(result.Domains) +} diff --git a/backend/internal/workflow/context.go b/backend/internal/workflow/context.go new file mode 100644 index 0000000..a86bfe2 --- /dev/null +++ b/backend/internal/workflow/context.go @@ -0,0 +1,33 @@ +package workflow + +import "ALLinSSL/backend/public" + +func NewExecutionContext(RunID string) *ExecutionContext { + Logger, _ := public.NewLogger(public.GetSettingIgnoreError("workflow_log_path") + RunID + ".log") + return &ExecutionContext{ + Data: make(map[string]any), + Status: make(map[string]ExecutionStatus), + RunID: RunID, + Logger: Logger, + } +} + +func (ctx *ExecutionContext) SetOutput(nodeID string, output any, status ExecutionStatus) { + ctx.mu.Lock() + defer ctx.mu.Unlock() + ctx.Data[nodeID] = output + ctx.Status[nodeID] = status +} + +func (ctx *ExecutionContext) GetOutput(nodeID string) (any, bool) { + ctx.mu.RLock() + defer ctx.mu.RUnlock() + out, ok := ctx.Data[nodeID] + return out, ok +} + +func (ctx *ExecutionContext) GetStatus(nodeID string) ExecutionStatus { + ctx.mu.RLock() + defer ctx.mu.RUnlock() + return ctx.Status[nodeID] +} diff --git a/backend/internal/workflow/executor.go b/backend/internal/workflow/executor.go new file mode 100644 index 0000000..8633bb1 --- /dev/null +++ b/backend/internal/workflow/executor.go @@ -0,0 +1,107 @@ +package workflow + +import ( + "ALLinSSL/backend/internal/cert" + certApply "ALLinSSL/backend/internal/cert/apply" + certDeploy "ALLinSSL/backend/internal/cert/deploy" + "ALLinSSL/backend/internal/report" + "ALLinSSL/backend/public" + "errors" + "fmt" +) + +// var executors map[string]func(map[string]any) (any, error) +// +// func RegistExector(executorName string, executor func(map[string]any) (any, error)) { +// executors[executorName] = executor +// } + +func Executors(exec string, params map[string]any) (any, error) { + switch exec { + case "apply": + return apply(params) + case "deploy": + return deploy(params) + case "upload": + return upload(params) + case "notify": + return notify(params) + default: + return nil, nil + } +} + +func apply(params map[string]any) (any, error) { + logger := params["logger"].(*public.Logger) + + logger.Info("=============申请证书=============") + certificate, err := certApply.Apply(params, logger) + if err != nil { + logger.Error(err.Error()) + logger.Info("=============申请失败=============") + return nil, err + } + logger.Info("=============申请成功=============") + return certificate, nil +} + +func deploy(params map[string]any) (any, error) { + logger := params["logger"].(*public.Logger) + logger.Info("=============部署证书=============") + certificate := params["certificate"] + if certificate == nil { + logger.Error("证书不存在") + logger.Info("=============部署失败=============") + return nil, errors.New("证书不存在") + } + err := certDeploy.Deploy(params, logger) + if err != nil { + logger.Error(err.Error()) + logger.Info("=============部署失败=============") + } else { + logger.Info("=============部署成功=============") + } + return nil, err +} + +func upload(params map[string]any) (any, error) { + logger := params["logger"].(*public.Logger) + logger.Info("=============上传证书=============") + + keyStr, ok := params["key"].(string) + if !ok { + logger.Error("上传的密钥有误") + logger.Info("=============上传失败=============") + return nil, errors.New("上传的密钥有误") + } + certStr, ok := params["cert"].(string) + if !ok { + logger.Error("上传的证书有误") + logger.Info("=============上传失败=============") + return nil, errors.New("上传的证书有误") + } + err := cert.UploadCert(keyStr, certStr) + if err != nil { + logger.Error(err.Error()) + logger.Info("=============上传失败=============") + return nil, err + } + logger.Info("=============上传成功=============") + + return params, nil +} + +func notify(params map[string]any) (any, error) { + // fmt.Println("通知:", params) + logger := params["logger"].(*public.Logger) + logger.Info("=============发送通知=============") + logger.Debug(fmt.Sprintf("发送通知:%s", params["subject"].(string))) + err := report.Notify(params) + if err != nil { + logger.Error(err.Error()) + logger.Info("=============发送失败=============") + return nil, err + } + logger.Info("=============发送成功=============") + return fmt.Sprintf("通知到: %s", params["message"]), nil +} diff --git a/backend/internal/workflow/models.go b/backend/internal/workflow/models.go new file mode 100644 index 0000000..4bc7dfa --- /dev/null +++ b/backend/internal/workflow/models.go @@ -0,0 +1,49 @@ +package workflow + +import ( + "ALLinSSL/backend/public" + "sync" +) + +type ExecutionStatus string + +const ( + StatusSuccess ExecutionStatus = "success" + StatusFailed ExecutionStatus = "fail" +) + +type WorkflowNodeParams struct { + Name string `json:"name"` + FromNodeID string `json:"fromNodeId,omitempty"` +} + +type WorkflowNode struct { + Id string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + + Config map[string]any `json:"config"` + Inputs []WorkflowNodeParams `json:"inputs"` + // Outputs []WorkflowNodeParams `json:"outputs"` + + ChildNode *WorkflowNode `json:"childNode,omitempty"` + ConditionNodes []*WorkflowNode `json:"conditionNodes,omitempty"` + + Validated bool `json:"validated"` +} + +type ExecutionContext struct { + Data map[string]any + Status map[string]ExecutionStatus + mu sync.RWMutex + RunID string + Logger *public.Logger +} + +type ExecTime struct { + Type string `json:"type"` + Month int `json:"month,omitempty"` + Week int `json:"week,omitempty"` + Hour int `json:"hour"` + Minute int `json:"minute"` +} diff --git a/backend/internal/workflow/workflow.go b/backend/internal/workflow/workflow.go new file mode 100644 index 0000000..2f4f692 --- /dev/null +++ b/backend/internal/workflow/workflow.go @@ -0,0 +1,294 @@ +package workflow + +import ( + "ALLinSSL/backend/public" + "encoding/json" + "fmt" + "strings" + "sync" + "time" +) + +func GetSqlite() (*public.Sqlite, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + s.TableName = "workflow" + return s, nil +} + +func GetList(search string, p, limit int64) ([]map[string]any, int, error) { + var data []map[string]any + var count int64 + s, err := GetSqlite() + if err != nil { + return data, 0, err + } + defer s.Close() + + var limits []int64 + if p >= 0 && limit >= 0 { + limits = []int64{0, limit} + if p > 1 { + limits[0] = (p - 1) * limit + limits[1] = p * limit + } + } + + if search != "" { + count, err = s.Where("name like ?", []interface{}{"%" + search + "%"}).Count() + data, err = s.Where("name like ?", []interface{}{"%" + search + "%"}).Order("update_time", "desc").Limit(limits).Select() + } else { + count, err = s.Count() + data, err = s.Order("update_time", "desc").Limit(limits).Select() + } + if err != nil { + return data, 0, err + } + return data, int(count), nil +} + +func AddWorkflow(name, content, execType, active, execTime string) error { + var node WorkflowNode + err := json.Unmarshal([]byte(content), &node) + if err != nil { + return fmt.Errorf("检测到工作流配置有问题:%v", err) + } + + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + now := time.Now().Format("2006-01-02 15:04:05") + _, err = s.Insert(map[string]interface{}{ + "name": name, + "content": content, + "exec_type": execType, + "active": active, + "exec_time": execTime, + "create_time": now, + "update_time": now, + }) + if err != nil { + return err + } + return nil +} + +func DelWorkflow(id string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + _, err = s.Where("id=?", []interface{}{id}).Delete() + if err != nil { + return err + } + return nil +} + +func UpdDb(id string, data map[string]any) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + data["update_time"] = time.Now().Format("2006-01-02 15:04:05") + _, err = s.Where("id=?", []interface{}{id}).Update(data) + if err != nil { + return err + } + return nil +} + +func UpdWorkflow(id, name, content, execType, active, execTime string) error { + var node WorkflowNode + err := json.Unmarshal([]byte(content), &node) + if err != nil { + return fmt.Errorf("检测到工作流配置有问题:%v", err) + } + err = UpdDb(id, map[string]interface{}{ + "name": name, + "content": content, + "exec_type": execType, + "active": active, + "exec_time": execTime, + }) + if err != nil { + return err + } + return nil +} + +func UpdExecType(id, execType string) error { + err := UpdDb(id, map[string]interface{}{ + "exec_type": execType, + }) + if err != nil { + return err + } + return nil +} + +func UpdActive(id, active string) error { + err := UpdDb(id, map[string]interface{}{ + "active": active, + }) + if err != nil { + return err + } + return nil +} + +func ExecuteWorkflow(id string) error { + s, err := GetSqlite() + if err != nil { + return err + } + defer s.Close() + data, err := s.Where("id=?", []interface{}{id}).Select() + if err != nil { + return err + } + if len(data) == 0 { + return fmt.Errorf("workflow not found") + } + if data[0]["last_run_status"] != nil && data[0]["last_run_status"].(string) == "running" { + return fmt.Errorf("工作流正在执行中") + } + content := data[0]["content"].(string) + + go func(id, c string) { + // defer wg.Done() + // WorkflowID := strconv.FormatInt(id, 10) + RunID, err := AddWorkflowHistory(id, "manual") + if err != nil { + return + } + ctx := NewExecutionContext(RunID) + defer ctx.Logger.Close() + err = RunWorkflow(c, ctx) + if err != nil { + fmt.Println("执行工作流失败:", err) + SetWorkflowStatus(id, RunID, "fail") + } else { + SetWorkflowStatus(id, RunID, "success") + } + }(id, content) + return nil +} + +func SetWorkflowStatus(id, RunID, status string) { + _ = UpdateWorkflowHistory(RunID, status) + _ = UpdDb(id, map[string]interface{}{"last_run_status": status}) +} + +func resolveInputs(inputs []WorkflowNodeParams, ctx *ExecutionContext) map[string]any { + resolved := make(map[string]any) + for _, input := range inputs { + if input.FromNodeID != "" { + if val, ok := ctx.GetOutput(input.FromNodeID); ok { + switch strings.Split(strings.TrimPrefix(input.FromNodeID, "-"), "-")[0] { + case "apply": + input.Name = "certificate" + case "upload": + input.Name = "certificate" + } + resolved[input.Name] = val + } + } + } + return resolved +} + +func RunNode(node *WorkflowNode, ctx *ExecutionContext) error { + // 获取上下文 + inputs := resolveInputs(node.Inputs, ctx) + // 组装参数 + if node.Config == nil { + node.Config = make(map[string]any) + } + for k, v := range inputs { + node.Config[k] = v + } + node.Config["_runId"] = ctx.RunID + node.Config["logger"] = ctx.Logger + + // 执行当前节点 + result, err := Executors(node.Type, node.Config) + + var status ExecutionStatus + if err != nil { + status = StatusFailed + if node.ChildNode == nil || node.ChildNode.Type != "execute_result_branch" { + return err + } + } else { + status = StatusSuccess + } + + ctx.SetOutput(node.Id, result, status) + + // 普通的并行 + if node.Type == "branch" { + if len(node.ConditionNodes) > 0 { + var wg sync.WaitGroup + errChan := make(chan error, len(node.ConditionNodes)) + for _, branch := range node.ConditionNodes { + wg.Add(1) + go func(node *WorkflowNode) { + defer wg.Done() + if err = RunNode(node, ctx); err != nil { + errChan <- err + } + }(branch) + } + wg.Wait() + close(errChan) + for err := range errChan { + if err != nil { + return err + } + } + } + } + // 条件分支 + if node.Type == "execute_result_branch" { + // + if len(node.ConditionNodes) > 0 { + lastStatus := ctx.GetStatus(node.Config["fromNodeId"].(string)) + for _, branch := range node.ConditionNodes { + if branch.Config["type"] == string(lastStatus) { + return RunNode(branch, ctx) + } + } + } + } + + if node.ChildNode != nil { + return RunNode(node.ChildNode, ctx) + } + return nil +} + +func RunWorkflow(content string, ctx *ExecutionContext) error { + var node WorkflowNode + err := json.Unmarshal([]byte(content), &node) + if err != nil { + return err + } else { + ctx.Logger.Info("=============开始执行=============") + err = RunNode(&node, ctx) + // fmt.Println(err) + if err != nil { + ctx.Logger.Info("=============执行失败=============") + return err + } + ctx.Logger.Info("=============执行完成=============") + return nil + } +} diff --git a/backend/internal/workflow/workflow_history.go b/backend/internal/workflow/workflow_history.go new file mode 100644 index 0000000..e7ae9e4 --- /dev/null +++ b/backend/internal/workflow/workflow_history.go @@ -0,0 +1,117 @@ +package workflow + +import ( + "ALLinSSL/backend/public" + "os" + "path/filepath" + "time" +) + +// GetSqliteObjWH 工作流执行历史记录表对象 +func GetSqliteObjWH() (*public.Sqlite, error) { + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + return nil, err + } + s.Connect() + s.TableName = "workflow_history" + return s, nil +} + +// GetListWH 获取工作流执行历史记录列表 +func GetListWH(id string, p, limit int64) ([]map[string]any, int, error) { + var data []map[string]any + var count int64 + s, err := GetSqliteObjWH() + if err != nil { + return data, 0, err + } + defer s.Close() + + var limits []int64 + if p >= 0 && limit >= 0 { + limits = []int64{0, limit} + if p > 1 { + limits[0] = (p - 1) * limit + limits[1] = p * limit + } + } + if id == "" { + count, err = s.Count() + data, err = s.Limit(limits).Order("create_time", "desc").Select() + } else { + count, err = s.Where("workflow_id=?", []interface{}{id}).Count() + data, err = s.Where("workflow_id=?", []interface{}{id}).Limit(limits).Order("create_time", "desc").Select() + } + + if err != nil { + return data, 0, err + } + return data, int(count), nil +} + +// 添加工作流执行历史记录 +func AddWorkflowHistory(workflowID, execType string) (string, error) { + s, err := GetSqliteObjWH() + if err != nil { + return "", err + } + defer s.Close() + now := time.Now().Format("2006-01-02 15:04:05") + ID := public.GenerateUUID() + _, err = s.Insert(map[string]interface{}{ + "id": ID, + "workflow_id": workflowID, + "status": "running", + "exec_type": execType, + "create_time": now, + }) + if err != nil { + return "", err + } + _ = UpdDb(workflowID, map[string]interface{}{"last_run_status": "running", "last_run_time": now}) + return ID, nil +} + +// 工作流执行结束 +func UpdateWorkflowHistory(id, status string) error { + s, err := GetSqliteObjWH() + if err != nil { + return err + } + defer s.Close() + now := time.Now().Format("2006-01-02 15:04:05") + _, err = s.Where("id=?", []interface{}{id}).Update(map[string]interface{}{ + "status": status, + "end_time": now, + }) + if err != nil { + return err + } + return nil +} + +func StopWorkflow(id string) error { + s, err := GetSqliteObjWH() + if err != nil { + return err + } + defer s.Close() + data, err := s.Where("id=?", []interface{}{id}).Select() + if err != nil { + return err + } + if len(data) == 0 { + return nil + } + SetWorkflowStatus(data[0]["workflow_id"].(string), id, "fail") + return nil +} + +func GetExecLog(id string) (string, error) { + log, err := os.ReadFile(filepath.Join(public.GetSettingIgnoreError("workflow_log_path"), id+".log")) + if err != nil { + return "", err + } + return string(log), nil +} diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go new file mode 100644 index 0000000..1dacb14 --- /dev/null +++ b/backend/middleware/auth.go @@ -0,0 +1,121 @@ +package middleware + +import ( + "ALLinSSL/backend/public" + "encoding/gob" + "fmt" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "net/http" + "strings" + "time" +) + +var Html404 = []byte(` +