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(` +404 Not Found + +

404 Not Found

+
AllinSSL
+ +`) + +func SessionAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + routePath := c.Request.URL.Path + method := c.Request.Method + paths := strings.Split(strings.TrimPrefix(routePath, "/"), "/") + session := sessions.Default(c) + now := time.Now() + gob.Register(time.Time{}) + last := session.Get("lastRequestTime") + var form struct { + Skip string `form:"skip"` + } + err := c.Bind(&form) + if err != nil { + fmt.Println(err) + } else { + if form.Skip == "1" { + c.Next() + return + } + } + + if routePath == public.Secure && session.Get("secure") == nil { + // 访问安全入口,设置 session + session.Set("secure", true) + session.Set("lastRequestTime", now) + // 一定要保存 session BEFORE redirect + session.Save() + // 返回登录页 + c.Redirect(http.StatusFound, "/login") + // c.Abort() + return + } else { + if session.Get("secure") == nil || last == nil { + c.Data(404, "text/html; charset=utf-8", Html404) + c.Abort() + return + } else { + if lastTime, ok := last.(time.Time); ok { + if now.Sub(lastTime) >= time.Second*time.Duration(public.TimeOut) { + if session.Get("login") == nil { + session.Clear() + session.Save() + c.Data(404, "text/html; charset=utf-8", Html404) + c.Abort() + return + } else { + session.Delete("login") + session.Set("lastRequestTime", now) + session.Save() + c.Redirect(http.StatusFound, "/login") + return + } + } else { + if session.Get("login") == nil { + if len(paths) > 0 { + if paths[0] == "login" { + c.Next() + return + } + } + if len(paths) > 1 { + if paths[1] == "login" { + c.Next() + return + } + } + // 判断是否为静态文件路径 + if method == "GET" { + if len(paths) > 1 && paths[0] == "static" { + c.Next() + return + } + } + // 返回登录页 + c.Redirect(http.StatusFound, "/login") + c.Abort() + return + } else { + if session.Get("__login_key") != public.GetSettingIgnoreError("login_key") { + session.Clear() + session.Save() + c.JSON(http.StatusUnauthorized, gin.H{"message": "登录信息发生变化,请重新登录"}) + c.Abort() + } else { + // 访问正常,更新最后请求时间 + session.Set("lastRequestTime", now) + session.Save() + } + } + } + } else { + c.Data(404, "text/html; charset=utf-8", Html404) + c.Abort() + return + } + } + } + } +} diff --git a/backend/middleware/log.go b/backend/middleware/log.go new file mode 100644 index 0000000..f67c5fd --- /dev/null +++ b/backend/middleware/log.go @@ -0,0 +1,37 @@ +package middleware + +import ( + "ALLinSSL/backend/public" + "fmt" + "time" + + "github.com/gin-gonic/gin" +) + +func LoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + c.Next() + + duration := time.Since(start) + method := c.Request.Method + path := c.Request.URL.Path + status := c.Writer.Status() + clientIP := c.ClientIP() + userAgent := c.Request.UserAgent() + respSize := c.Writer.Size() // 响应体字节大小 + + msg := fmt.Sprintf( + "| %3d | %13v | %15s | %-7s %-30s | UA: %-40s | RespSize: %d bytes", + status, + duration, + clientIP, + method, + path, + userAgent, + respSize, + ) + + public.Info(msg) + } +} diff --git a/backend/middleware/oplog.go b/backend/middleware/oplog.go new file mode 100644 index 0000000..01b61b1 --- /dev/null +++ b/backend/middleware/oplog.go @@ -0,0 +1,10 @@ +package middleware + +// import ( +// "github.com/gin-gonic/gin" +// ) +// +// func OpLoggerMiddleware() gin.HandlerFunc { +// return func(c *gin.Context) { +// } +// } diff --git a/backend/migrations/init.go b/backend/migrations/init.go new file mode 100644 index 0000000..ddda419 --- /dev/null +++ b/backend/migrations/init.go @@ -0,0 +1,223 @@ +package migrations + +import ( + "ALLinSSL/backend/public" + "database/sql" + "fmt" + _ "github.com/mattn/go-sqlite3" + "os" + "path/filepath" +) + +func init() { + os.MkdirAll("data", os.ModePerm) + + dbPath := "data/data.db" + _, _ = filepath.Abs(dbPath) + // fmt.Println("数据库路径:", absPath) + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + // fmt.Println("创建数据库失败:", err) + return + } + defer db.Close() + // 创建表 + _, err = db.Exec(` + create table IF NOT EXISTS _accounts + ( + id integer not null + constraint _accounts_pk + primary key autoincrement, + private_key TEXT not null, + reg TEXT not null, + email TEXT not null, + create_time TEXT, + update_time TEXT, + type TEXT + ); + + create table IF NOT EXISTS access + ( + id integer not null + constraint access_pk + primary key autoincrement, + config TEXT not null, + type TEXT not null, + create_time TEXT, + update_time TEXT, + name TEXT not null + ); + + create table IF NOT EXISTS access_type + ( + id integer not null + constraint access_type_pk + primary key autoincrement, + name TEXT, + type TEXT + ); + + create table IF NOT EXISTS cert + ( + id integer not null + constraint cert_pk + primary key autoincrement, + source TEXT not null, + sha256 TEXT, + history_id TEXT, + key TEXT not null, + cert TEXT not null, + issuer_cert integer, + domains TEXT not null, + create_time TEXT, + update_time TEXT, + issuer TEXT not null, + start_time TEXT, + end_time TEXT, + end_day TEXT, + workflow_id TEXT + ); + + create table IF NOT EXISTS report + ( + id integer not null + constraint report_pk + primary key autoincrement, + type TEXT not null, + config TEXT not null, + create_time TEXT, + update_time TEXT, + name TEXT + ); + + create table IF NOT EXISTS settings + ( + id integer + constraint settings_pk + primary key, + key TEXT, + value TEXT, + create_time TEXT not null, + update_time TEXT not null, + active integer not null, + type TEXT + ); + + create table IF NOT EXISTS site_monitor + ( + id integer not null + constraint site_monitor_pk + primary key autoincrement, + name TEXT not null, + site_domain TEXT not null, + cycle integer not null, + report_type TEXT not null, + cert_domain TEXT, + ca TEXT, + active integer, + end_time TEXT, + end_day TEXT, + last_time TEXT, + except_end_time TEXT, + create_time TEXT, + state TEXT, + update_time TEXT, + repeat_send_gap INTEGER + ); + + create table IF NOT EXISTS users + ( + id integer not null + constraint users_pk + primary key autoincrement, + username TEXT not null + constraint users_pk2 + unique, + password TEXT not null, + salt TEXT default '' not null + ); + + + create table IF NOT EXISTS workflow + ( + id integer not null + constraint workflow_pk + primary key autoincrement, + name TEXT not null, + content TEXT not null, + cron TEXT, + create_time TEXT, + update_time TEXT, + active integer, + exec_type TEXT, + last_run_status TEXT, + exec_time TEXT, + last_run_time TEXT + ); + + create table IF NOT EXISTS workflow_history + ( + id TEXT not null + constraint work_flow_pk + primary key, + status TEXT, + exec_type TEXT, + create_time TEXT, + end_time TEXT, + workflow_id TEXT not null + fail_reason TEXT, + ); + `) + insertDefaultData(db, "users", "INSERT INTO users (id, username, password, salt) VALUES (1, 'xxxx', 'xxxxxxx', '&*ghs^&%dag');") + insertDefaultData(db, "access_type", ` + INSERT INTO access_type (name, type) VALUES ('aliyun', 'dns'); + INSERT INTO access_type (name, type) VALUES ('tencentcloud', 'dns'); + INSERT INTO access_type (name, type) VALUES ('aliyun', 'host'); + INSERT INTO access_type (name, type) VALUES ('tencentcloud', 'host'); + INSERT INTO access_type (name, type) VALUES ('ssh', 'host'); + INSERT INTO access_type (name, type) VALUES ('btpanel', 'host'); + INSERT INTO access_type (name, type) VALUES ('1panel', 'host');`) + + uuidStr := public.GenerateUUID() + randomStr := public.RandomString(8) + + port, err := public.GetFreePort() + if err != nil { + port = 20773 + } + + Isql := fmt.Sprintf( + `INSERT INTO settings (key, value, create_time, update_time, active, type) VALUES ('log_path', 'logs/ALLinSSL.log', '2025-04-15 15:58', '2025-04-15 15:58', 1, null); +INSERT INTO settings (key, value, create_time, update_time, active, type) VALUES ( 'workflow_log_path', 'logs/workflows/', '2025-04-15 15:58', '2025-04-15 15:58', 1, null); +INSERT INTO settings (key, value, create_time, update_time, active, type) VALUES ( 'timeout', '3600', '2025-04-15 15:58', '2025-04-15 15:58', 1, null); +INSERT INTO settings (key, value, create_time, update_time, active, type) VALUES ( 'https', '0', '2025-04-15 15:58', '2025-04-15 15:58', 1, null); +INSERT INTO settings (key, value, create_time, update_time, active, type) VALUES ( 'login_key', '%s', '2025-04-15 15:58', '2025-04-15 15:58', 1, null); +INSERT INTO settings (key, value, create_time, update_time, active, type) VALUES ('session_key', '%s', '2025-04-15 15:58', '2025-04-15 15:58', 1, null); +INSERT INTO settings (key, value, create_time, update_time, active, type) VALUES ('secure', '/%s', '2025-04-15 15:58', '2025-04-15 15:58', 1, null); +INSERT INTO settings (key, value, create_time, update_time, active, type) VALUES ('port', '%d', '2025-04-15 15:58', '2025-04-15 15:58', 1, null);`, uuidStr, uuidStr, randomStr, port) + + insertDefaultData(db, "settings", Isql) +} + +func insertDefaultData(db *sql.DB, table, insertSQL string) { + // 查询用户表中现有的记录数 + var count int + err := db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count) + if err != nil { + // fmt.Println("检查数据行数失败:", err) + return + } + + // 如果表为空,则插入默认数据 + if count == 0 { + // fmt.Println("表为空,插入默认数据...") + _, err = db.Exec(insertSQL) + if err != nil { + // fmt.Println("插入数据失败:", err) + return + } + // fmt.Println("默认数据插入成功。") + // } else { + // fmt.Println("表已有数据,跳过插入。") + } +} diff --git a/backend/public/cert.go b/backend/public/cert.go new file mode 100644 index 0000000..73a7978 --- /dev/null +++ b/backend/public/cert.go @@ -0,0 +1,139 @@ +package public + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "time" +) + +// **解析 PEM 格式的证书** +func ParseCertificate(certPEM []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(certPEM) + if block == nil { + return nil, fmt.Errorf("无法解析证书 PEM") + } + return x509.ParseCertificate(block.Bytes) +} + +// **解析 PEM 格式的私钥** +func ParsePrivateKey(keyPEM []byte) (crypto.PrivateKey, error) { + block, _ := pem.Decode(keyPEM) + if block == nil { + return nil, fmt.Errorf("无法解析私钥 PEM") + } + + // **尝试解析不同类型的私钥** + if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { + return key, nil + } + if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil { + return key, nil + } + if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { + return key, nil + } + return nil, fmt.Errorf("无法识别的私钥格式") +} + +// **检查证书是否过期** +func CheckCertificateExpiration(cert *x509.Certificate) error { + now := time.Now() + if now.Before(cert.NotBefore) { + return fmt.Errorf("证书尚未生效,有效期开始于: %v", cert.NotBefore) + } + if now.After(cert.NotAfter) { + return fmt.Errorf("证书已过期,有效期截止到: %v", cert.NotAfter) + } + return nil +} + +// **检查证书是否与私钥匹配** +func VerifyCertificateAndKey(cert *x509.Certificate, privateKey crypto.PrivateKey) error { + messageARR := sha256.Sum256([]byte("test message")) + message := messageARR[:] + var signature []byte + var err error + + // **用私钥签名数据** + switch key := privateKey.(type) { + case *rsa.PrivateKey: + signature, err = rsa.SignPKCS1v15(nil, key, crypto.SHA256, message) + case *ecdsa.PrivateKey: + signature, err = key.Sign(nil, message, crypto.SHA256) + case ed25519.PrivateKey: + signature = ed25519.Sign(key, message) + default: + err = errors.New("不支持的私钥类型") + } + if err != nil { + return fmt.Errorf("签名失败: %v", err) + } + + // **使用公钥验证签名** + switch pub := cert.PublicKey.(type) { + case *rsa.PublicKey: + err = rsa.VerifyPKCS1v15(pub, crypto.SHA256, message, signature) + case *ecdsa.PublicKey: + ok := ecdsa.VerifyASN1(pub, message, signature) + if !ok { + err = fmt.Errorf("ECDSA 签名验证失败") + } + case ed25519.PublicKey: + if !ed25519.Verify(pub, message, signature) { + err = fmt.Errorf("Ed25519 签名验证失败") + } + default: + err = fmt.Errorf("不支持的公钥类型: %T", pub) + } + return err +} + +// **主验证函数** +func ValidateSSLCertificate(certStr, keyStr string) error { + certPEM, keyPEM := []byte(certStr), []byte(keyStr) + // **解析证书和私钥** + cert, err := ParseCertificate(certPEM) + if err != nil { + return fmt.Errorf("解析证书失败: %v", err) + } + privateKey, err := ParsePrivateKey(keyPEM) + if err != nil { + return fmt.Errorf("解析私钥失败: %v", err) + } + + // **检查证书有效期** + if err := CheckCertificateExpiration(cert); err != nil { + return err + } + + // **检查证书和私钥是否匹配** + if err := VerifyCertificateAndKey(cert, privateKey); err != nil { + return fmt.Errorf("证书与私钥不匹配: %v", err) + } + + return nil +} + +// 获取sha256 +func GetSHA256(certStr string) (string, error) { + certPEM := []byte(certStr) + block, _ := pem.Decode(certPEM) + if block == nil { + return "", fmt.Errorf("无法解析证书 PEM") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return "", fmt.Errorf("解析证书失败: %v", err) + } + + sha256Hash := sha256.Sum256(cert.Raw) + return hex.EncodeToString(sha256Hash[:]), nil +} diff --git a/backend/public/config.go b/backend/public/config.go new file mode 100644 index 0000000..85d1240 --- /dev/null +++ b/backend/public/config.go @@ -0,0 +1,44 @@ +package public + +import "strconv" + +var Port = GetSettingIgnoreError("port") +var Secure = GetSettingIgnoreError("secure") +var SessionKey = GetSettingIgnoreError("session_key") +var LogPath = GetSettingIgnoreError("log_path") +var TimeOut = func() int { + settingStr := GetSettingIgnoreError("timeout") + setting, err := strconv.Atoi(settingStr) + if err != nil { + return 300 + } + return setting +}() +var ShutdownFunc func() + +func ReloadConfig() { + Port = GetSettingIgnoreError("port") + Secure = GetSettingIgnoreError("secure") + SessionKey = GetSettingIgnoreError("session_key") + LogPath = GetSettingIgnoreError("log_path") + + settingStr := GetSettingIgnoreError("timeout") + setting, err := strconv.Atoi(settingStr) + if err != nil { + TimeOut = 300 + } else { + TimeOut = setting + } + ShutdownFunc = nil + +} + +// OpLog 操作日志 +type OpLog struct { + OpType string `db:"op_type"` + OpUser string `db:"op_user"` + OpTime string `db:"op_time"` + OpDetail string `db:"op_detail"` + OpResult string `db:"op_result"` + IP string `db:"ip"` +} diff --git a/backend/public/logger.go b/backend/public/logger.go new file mode 100644 index 0000000..2b8a126 --- /dev/null +++ b/backend/public/logger.go @@ -0,0 +1,112 @@ +package public + +import ( + "log" + "os" + "path/filepath" + "sync" + "time" +) + +var ( + infoLogger *log.Logger + errorLogger *log.Logger + warnLogger *log.Logger + logFile *os.File +) + +// InitLogger 初始化日志器(仅写入文件) +func InitLogger(logPath string) { + // 确保日志目录存在 + dir := filepath.Dir(logPath) + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + panic("创建日志目录失败: " + err.Error()) + } + + var err error + logFile, err = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + panic("无法打开日志文件: " + err.Error()) + } + + infoLogger = log.New(logFile, "[INFO] ", log.LstdFlags|log.Lshortfile) + errorLogger = log.New(logFile, "[ERROR] ", log.LstdFlags|log.Lshortfile) + warnLogger = log.New(logFile, "[WARN] ", log.LstdFlags|log.Lshortfile) +} + +// Info 输出 Info 级别日志 +func Info(msg string) { + infoLogger.Println(msg) +} + +// Error 输出 Error 级别日志 +func Error(msg string) { + errorLogger.Println(msg) +} + +// Warn 输出 Warn 级别日志 +func Warn(msg string) { + warnLogger.Println(msg) +} + +// CloseLogger 关闭日志文件(建议在程序退出前调用) +func CloseLogger() { + if logFile != nil { + logFile.Close() + } +} + +type Logger struct { + filePath string + logger *log.Logger + file *os.File + mutex sync.Mutex +} + +func NewLogger(filePath string) (*Logger, error) { + // 确保日志目录存在 + dir := filepath.Dir(filePath) + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + panic("创建日志目录失败: " + err.Error()) + } + + file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, err + } + + l := log.New(file, "", 0) // 不设置前缀,我们手动格式化 + return &Logger{ + filePath: filePath, + logger: l, + file: file, + }, nil +} + +// Close 关闭日志文件 +func (l *Logger) Close() { + l.file.Close() +} + +// write 写日志,内部使用锁保证线程安全 +func (l *Logger) write(level string, msg string) { + l.mutex.Lock() + defer l.mutex.Unlock() + + timestamp := time.Now().Format("2006-01-02 15:04:05") + logLine := "[" + level + "] " + timestamp + " - " + msg + l.logger.Println(logLine) +} + +// Info 输出 info 级别日志 +func (l *Logger) Info(msg string) { + l.write("INFO", msg) +} + +// Error 输出 error 级别日志 +func (l *Logger) Error(msg string) { + l.write("ERROR", msg) +} +func (l *Logger) Debug(msg string) { + l.write("Debug", msg) +} diff --git a/backend/public/response.go b/backend/public/response.go new file mode 100644 index 0000000..84db96d --- /dev/null +++ b/backend/public/response.go @@ -0,0 +1,44 @@ +package public + +import "github.com/gin-gonic/gin" + +type Response struct { + Code int `json:"code"` + Count int `json:"count"` + Data any `json:"data"` + Message string `json:"message"` + Status bool `json:"status"` +} + +func SuccessMsg(c *gin.Context, msg string) { + c.JSON(200, Response{ + Code: 200, + Count: 0, + Data: nil, + Message: msg, + Status: true, + }) + c.Abort() +} + +func SuccessData(c *gin.Context, data interface{}, count int) { + c.JSON(200, Response{ + Code: 200, + Count: count, + Data: data, + Message: "success", + Status: true, + }) + c.Abort() +} + +func FailMsg(c *gin.Context, msg string) { + c.JSON(500, Response{ + Code: 500, + Count: 0, + Data: nil, + Message: msg, + Status: false, + }) + c.Abort() +} diff --git a/backend/public/sqlite.go b/backend/public/sqlite.go new file mode 100644 index 0000000..c4a121b --- /dev/null +++ b/backend/public/sqlite.go @@ -0,0 +1,1094 @@ +package public + +import ( + "database/sql" + "errors" + "fmt" + _ "modernc.org/sqlite" + "os" + "strconv" + "strings" +) + +/* +* + + - @brief Sqlite对象 + 示例: + fmt.Println(data) +*/ +type Sqlite struct { + DbFile string + PreFix string + TableName string + Conn *sql.DB + JoinTable []string + JoinOn []string + JoinType []string + JoinParam []interface{} + OptField []string + OptLimit string + OptWhere string + OptOrder string + OptParam []interface{} + OptGroupBy string + OptHaving string + Tx *sql.Tx + TxErr error + Sql string + closed bool +} + +/** + * @brief 实例化Sqlite对象 + * @param DbFile string 数据库文件 + * @param PreFix string 表前缀 + * @return *Sqlite 实例化的Sqlite对象 + * @return error 错误信息 + */ +func NewSqlite(DbFile string, PreFix string) (*Sqlite, error) { + s := Sqlite{} + s.DbFile = DbFile + s.PreFix = PreFix + s.closed = true + if !FileExists(DbFile) { + return nil, errors.New("错误:指定数据库文件不存在") + } + err := s.Connect() + if err != nil { + return nil, err + } + + return &s, nil +} + +func FileExists(file string) bool { + _, err := os.Stat(file) + if err == nil { + return true + } + return false +} + +/** + * @brief 连接数据库 + * @return error 错误信息 + */ +func (s *Sqlite) Connect() error { + conn, err := sql.Open("sqlite", s.DbFile) + if err == nil { + s.Conn = conn + s.closed = false + } + return err +} + +/** + * @brief 关闭数据库连接 + */ +func (s *Sqlite) Close() { + s.Conn.Close() + s.closed = true + s.Tx = nil +} + +/** + * @brief 设置数据库主机地址 + * @param Host string 主机地址 + * @param Port int 端口号 + * @return void + */ +func (s *Sqlite) SetDbFile(DbFile string) { + if !FileExists(DbFile) { + panic("错误:指定数据库文件不存在") + } + s.DbFile = DbFile +} + +/** + * @brief 设置要操作的表名 + * @param tableName string 表名 + * @return *Sqlite 实例化的Sqlite对象 + */ +func (s *Sqlite) Table(tableName string) *Sqlite { + s.TableName = s.PreFix + tableName + return s +} + +/** + * @brief 设置要返回的字段 + * @param field []string 字段名数组 + * @return *Sqlite 实例化的Sqlite对象 + */ +func (s *Sqlite) Field(field []string) *Sqlite { + s.OptField = field + return s +} + +/** + * @brief 设置取回行数 + * @param limit []int64 ,limit[0]为起始行数,limit[1]为取回行数 + * @return *Sqlite 实例化的Sqlite对象 + */ +func (s *Sqlite) Limit(limit []int64) *Sqlite { + last_limit := " LIMIT " + limit_len := len(limit) + if limit_len == 0 { + s.OptLimit = "" + } else if limit_len == 1 { + s.OptLimit = last_limit + strconv.FormatInt(limit[0], 10) + } else if limit_len >= 2 { + s.OptLimit = last_limit + strconv.FormatInt(limit[0], 10) + "," + strconv.FormatInt(limit[1], 10) + } else { + s.OptLimit = "" + } + return s +} + +/** + * @brief 设置排序 + * @param fieldName string 字段名 + * @param sortOrder string 排序方式,ASC为升序,DESC为降序 + * @return *Sqlite 实例化的Sqlite对象 + */ +func (s *Sqlite) Order(fieldName string, sortOrder string) *Sqlite { + sortOrder = strings.ToUpper(sortOrder) + if sortOrder != "ASC" && sortOrder != "DESC" { + sortOrder = "ASC" + } + s.OptOrder = " ORDER BY " + fieldName + " " + sortOrder + return s +} + +/** + * @brief 设置排序 + * @param fieldName string 字段名 + * @param sortOrder string 排序方式,ASC为升序,DESC为降序 + * @return *Sqlite 实例化的Sqlite对象 + */ +func (s *Sqlite) Sort(fieldName string, sortOrder string) *Sqlite { + return s.Order(fieldName, sortOrder) +} + +/** + * @brief 设置查询条件 + * @param where string 查询条件 + * @param param []interface{} 查询条件参数 + * @return *Sqlite 实例化的Sqlite对象 + */ +func (s *Sqlite) Where(where string, param []interface{}) *Sqlite { + s.OptWhere = " WHERE " + where + s.OptParam = param + return s +} + +/** + * @brief 获取查询字段 + * @return string 查询字段 + */ +func (s *Sqlite) getField() string { + field := "*" + if len(s.OptField) > 0 { + field = strings.Join(s.OptField, ",") + } + return field +} + +func (s *Sqlite) getJoinOn(sql string) string { + // 处理JOIN + for i := 0; i < len(s.JoinTable); i++ { + if s.JoinTable[i] == "" { + continue + } + + // 处理JOIN类型 + if s.JoinType[i] != "" { + s.JoinType[i] = " " + s.JoinType[i] + } + + // 拼接JOIN ON + sql += " " + s.JoinType[i] + " JOIN " + s.JoinTable[i] + " ON " + s.JoinOn[i] + } + + // 处理JOIN参数 + if s.JoinParam != nil { + joinParamLen := len(s.JoinParam) + optParamLen := len(s.OptParam) + params := make([]interface{}, 0) + if joinParamLen > 0 { + params = append(params, s.JoinParam...) + } + if optParamLen > 0 { + params = append(params, s.JoinParam...) + } + + s.OptParam = params + } + return sql +} + +/** + * @brief 获取查询条件 + * @param sql string sql语句 + * @return string 拼接查询条件后的sql语句 + */ +func (s *Sqlite) getWhere(sql string) string { + if s.OptWhere != "" { + sql += s.OptWhere + } + return sql +} + +func (s *Sqlite) getGroup(sql string) string { + if s.OptGroupBy != "" { + sql += " " + s.OptGroupBy + " " + } + return sql +} + +/** + * @brief 获取排序 + * @param sql string sql语句 + * @return string 拼接排序后的sql语句 + */ +func (s *Sqlite) getOrder(sql string) string { + if s.OptOrder != "" { + sql += s.OptOrder + } + return sql +} + +/** + * @brief 获取行数 + * @param sql string sql语句 + * @return string 拼接行数后的sql语句 + */ +func (s *Sqlite) getLimit(sql string) string { + if s.OptLimit != "" { + sql += s.OptLimit + } + return sql +} + +func (s *Sqlite) checkParam(sql string, param []interface{}) error { + // 检查参数数量 + sqlCount := strings.Count(sql, "?") + paramCount := len(param) + + if sqlCount != paramCount { + return errors.New("参数数量不匹配,要求绑定" + strconv.Itoa(sqlCount) + "个参数,实际绑定" + strconv.Itoa(paramCount) + "个参数") + } + return nil +} + +/** + * @brief 获取数据集 + * @return []map[string]interface{} 数据集 + */ +func (s *Sqlite) Select() ([]map[string]interface{}, error) { + if s.TableName == "" { + return nil, errors.New("错误:未指定要操作的表名") + } + field := s.getField() + + s.Sql = "SELECT " + field + " FROM " + s.TableName + + s.Sql = s.getJoinOn(s.Sql) + s.Sql = s.getWhere(s.Sql) + s.Sql = s.getGroup(s.Sql) + s.Sql = s.getOrder(s.Sql) + s.Sql = s.getLimit(s.Sql) + defer s.clearOpt() + // fmt.Println(s.Sql, s.OptParam) + + // 检查参数数量 + err := s.checkParam(s.Sql, s.OptParam) + if err != nil { + return nil, err + } + + // 清空参数 + rows, err := s.Conn.Query(s.Sql, s.OptParam...) + if err != nil { + return nil, err + } + defer rows.Close() + columns, _ := rows.Columns() + count := len(columns) + values := make([]interface{}, count) + valuePtrs := make([]interface{}, count) + var result []map[string]interface{} + for rows.Next() { + for i := 0; i < count; i++ { + valuePtrs[i] = &values[i] + } + rows.Scan(valuePtrs...) + row := make(map[string]interface{}) + for i, col := range columns { + var v interface{} + val := values[i] + b, ok := val.([]byte) + if ok { + v = string(b) + } else { + v = val + } + row[col] = v + } + result = append(result, row) + } + return result, err +} + +/** + * @brief 插入数据 + * @param data map[string]interface{} 要插入的数据 + * @return int64 插入的数据ID + * @return error 错误信息 + */ +func (s *Sqlite) Insert(data map[string]interface{}) (int64, error) { + + if s.TableName == "" { + return 0, errors.New("错误:未指定要操作的表名") + } + + var keys []string + var values []interface{} + var placeholders []string + for k, v := range data { + keys = append(keys, k) + values = append(values, v) + placeholders = append(placeholders, "?") + } + + // 清空参数 + defer s.clearOpt() + + s.Sql = "INSERT INTO " + s.TableName + " (" + strings.Join(keys, ",") + ") VALUES (" + strings.Join(placeholders, ",") + ")" + stmt, err := s.Conn.Prepare(s.Sql) + if err != nil { + return 0, err + } + defer stmt.Close() + res, err := stmt.Exec(values...) + if err != nil { + return 0, err + } + + id, err := res.LastInsertId() + + return id, err +} + +/** + * @brief 更新数据 + * @param data map[string]interface{} 要更新的数据 + * @return int64 影响的行数 + * @return error 错误信息 + */ +func (s *Sqlite) Update(data map[string]interface{}) (int64, error) { + if s.TableName == "" { + return 0, errors.New("错误:未指定要操作的表名") + } + var keys []string + var values []interface{} + for k, v := range data { + keys = append(keys, k) + values = append(values, v) + } + s.Sql = "UPDATE " + s.TableName + " SET " + strings.Join(keys, "=?,") + "=?" + if s.OptWhere != "" { + s.Sql += s.OptWhere + } + + // 清空参数 + defer s.clearOpt() + + stmt, err := s.Conn.Prepare(s.Sql) + if err != nil { + return 0, err + } + defer stmt.Close() + values = append(values, s.OptParam...) + + // 检查参数数量 + err = s.checkParam(s.Sql, values) + if err != nil { + return 0, err + } + + res, err := stmt.Exec(values...) + if err != nil { + return 0, err + } + return res.RowsAffected() +} + +/** + * @brief 插入更新 + * @param data map[string]interface{} 要更新的数据 + * @param uniqueIdx 依据的唯一索引的字段信息,如果数据库中不存在该索引,则会出现报错 如:(date, uri_id) + * @param updateFormula 更新字段的表达式,如: request = request + excluded.request + * @return int64 影响的行数 + * @return error 错误信息 + */ +func (s *Sqlite) Upsert(uniqueIdx []string, data map[string]interface{}, updateFormula string) (int64, error) { + if s.TableName == "" { + return 0, errors.New("错误:未指定要操作的表名") + } + + var keys []string + var values []interface{} + placeholdersBuilder := strings.Builder{} + var first = true + for k, v := range data { + keys = append(keys, k) + values = append(values, v) + if first { + placeholdersBuilder.Write([]byte("?")) + first = false + } else { + placeholdersBuilder.Write([]byte(",?")) + } + } + placeholders := placeholdersBuilder.String() + + // 清空参数 + defer s.clearOpt() + + s.Sql = fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s", + s.TableName, strings.Join(keys, ","), placeholders, strings.Join(uniqueIdx, ","), updateFormula) + stmt, err := s.Conn.Prepare(s.Sql) + if err != nil { + return 0, err + } + defer stmt.Close() + res, err := stmt.Exec(values...) + if err != nil { + return 0, err + } + + id, err := res.LastInsertId() + + return id, err +} + +/** + * @brief 删除数据 + * @return int64 影响的行数 + * @return error 错误信息 + */ +func (s *Sqlite) Delete() (int64, error) { + if s.TableName == "" { + return 0, errors.New("错误:未指定要操作的表名") + } + s.Sql = "DELETE FROM " + s.TableName + if s.OptWhere != "" { + s.Sql += s.OptWhere + } + + // 清空参数 + defer s.clearOpt() + + // 检查参数数量 + err := s.checkParam(s.Sql, s.OptParam) + if err != nil { + return 0, err + } + + // 预编译SQL语句 + stmt, err := s.Conn.Prepare(s.Sql) + if err != nil { + return 0, err + } + defer stmt.Close() + + // 执行SQL语句 + res, err := stmt.Exec(s.OptParam...) + if err != nil { + return 0, err + } + return res.RowsAffected() +} + +/** + * @brief 获取一条数据 + * @return sap[string]interface{} 数据集 + * @return error 错误信息 + */ +func (s *Sqlite) Find() (map[string]interface{}, error) { + s.Limit([]int64{1}) + result, err := s.Select() + if err != nil { + return nil, err + } + + if len(result) > 0 { + return result[0], nil + } + + return nil, errors.New("not found") +} + +/** + * @brief 获取总数 + * @return int64 总数 + * @return error 错误信息 + */ +func (s *Sqlite) Count() (int64, error) { + if s.TableName == "" { + return 0, errors.New("错误:未指定要操作的表名") + } + s.Sql = "SELECT COUNT(*) FROM " + s.TableName + if s.OptWhere != "" { + s.Sql += s.OptWhere + } + defer s.clearOpt() + + // 检查参数数量 + err := s.checkParam(s.Sql, s.OptParam) + if err != nil { + return 0, err + } + + // 执行SQL语句 + rows, err := s.Conn.Query(s.Sql, s.OptParam...) + if err != nil { + return 0, err + } + defer rows.Close() + var count int64 + for rows.Next() { + rows.Scan(&count) + } + return count, err +} + +// CountField 获取指定字段的总数 +func (s *Sqlite) CountField(field string) (int64, error) { + if s.TableName == "" { + return 0, errors.New("错误:未指定要操作的表名") + } + if field == "" { + field = "*" + } + s.Sql = "SELECT COUNT(" + field + ") FROM " + s.TableName + if s.OptWhere != "" { + s.Sql += s.OptWhere + } + defer s.clearOpt() + + // 检查参数数量 + err := s.checkParam(s.Sql, s.OptParam) + if err != nil { + return 0, err + } + + // 执行SQL语句 + rows, err := s.Conn.Query(s.Sql, s.OptParam...) + if err != nil { + return 0, err + } + defer rows.Close() + var count int64 + for rows.Next() { + rows.Scan(&count) + } + return count, err +} + +// CountFields 获取多个字段的统计总数 +func (s *Sqlite) CountFields(fields []string) ([]int64, error) { + if s.TableName == "" { + return nil, errors.New("错误:未指定要操作的表名") + } + countStrBuilder := strings.Builder{} + if len(fields) == 0 { + return nil, errors.New("错误:未指定要统计的字段") + } else { + for _, field := range fields { + if countStrBuilder.Len() > 0 { + countStrBuilder.WriteByte(',') + } + countStrBuilder.WriteString(fmt.Sprintf("COUNT(%s)", field)) + } + } + s.Sql = "SELECT " + countStrBuilder.String() + " FROM " + s.TableName + if s.OptWhere != "" { + s.Sql += s.OptWhere + } + defer s.clearOpt() + + // 检查参数数量 + err := s.checkParam(s.Sql, s.OptParam) + if err != nil { + return nil, err + } + + // 执行SQL语句 + rows, err := s.Conn.Query(s.Sql, s.OptParam...) + if err != nil { + return nil, err + } + defer rows.Close() + countList := make([]int64, len(fields)) + tmpList := make([]any, len(fields)) + for i := 0; i < len(fields); i++ { + tmpList[i] = &countList[i] + } + + for rows.Next() { + rows.Scan(tmpList...) + } + return countList, err +} + +/** + * @brief 获取指定字段的值 + * @param field string 字段名 + * @return interface{} 字段值 + * @return error 错误信息 + */ +func (s *Sqlite) Value(field string) (interface{}, error) { + s.Field([]string{field}) + s.Limit([]int64{1}) + result, err := s.Select() + if err != nil { + return nil, err + } + + if len(result) > 0 { + return result[0][field], nil + } + + return nil, errors.New("not found") +} + +/** + * @brief 设置指定字段的值 + * @param field string 字段名 + * @param value interface{} 字段值 + * @return int64 影响的行数 + * @return error 错误信息 + */ +func (s *Sqlite) SetValue(field string, value interface{}) (int64, error) { + data := make(map[string]interface{}) + data[field] = value + return s.Update(data) +} + +/** + * @brief 启动事务 + * @return error 错误信息 + */ +func (s *Sqlite) Begin() (*sql.Tx, error) { + s.Tx, s.TxErr = s.Conn.Begin() + return s.Tx, s.TxErr +} + +/** + * @brief 使用事务批量插入数据 + * @param values [][]interface{} 要插入的数据 二维数组,每个数组为一条数据,数组中的值为字段值,顺序与keys对应 + * @param keys []string 字段名 + * @return int 影响的行数 + * @return error 错误信息 + */ +func (s *Sqlite) InsertBegin(values [][]interface{}, keys []string) (int, []int64, error) { + if s.TableName == "" { + return 0, nil, errors.New("错误:未指定要操作的表名") + } + var placeholders []string + // 遍历数据 + for range keys { + placeholders = append(placeholders, "?") + } + + // 启动事务 + s.Tx, s.TxErr = s.Conn.Begin() + if s.TxErr != nil { + return 0, nil, s.TxErr + } + + // 清空参数 + defer s.clearOpt() + + // 预编译SQL语句 + s.Sql = "INSERT INTO " + s.TableName + " (" + strings.Join(keys, ",") + ") VALUES (" + strings.Join(placeholders, ",") + ")" + stmt, err := s.Tx.Prepare(s.Sql) + if err != nil { + return 0, nil, err + } + defer func(stmt *sql.Stmt) { + err := stmt.Close() + if err != nil { + println("结束报错", err.Error()) + } + }(stmt) + + // 执行SQL语句 + insertIds := make([]int64, 0, len(values)) + ok := 0 + for _, val := range values { + tmp, err := stmt.Exec(val...) + if err != nil { + // 显示回滚任务, 避免数据库事务未完成导致资源占 + err2 := s.Tx.Rollback() + s.Tx = nil + if err2 != nil { + fmt.Println("回滚报错", err2.Error()) + } + return 0, nil, err + } + tmpId, _ := tmp.LastInsertId() + insertIds = append(insertIds, tmpId) + ok++ + } + + // 提交事务 + err = s.Tx.Commit() + s.Tx = nil + + return ok, insertIds, err +} + +/** + * @brief 使用事务批量插入数据 并忽略 可能出现的唯一索引错误的问题 + * @param values [][]interface{} 要插入的数据 二维数组,每个数组为一条数据,数组中的值为字段值,顺序与keys对应 + * @param keys []string 字段名 + * @return int 影响的行数 + * @return error 错误信息 + */ +func (s *Sqlite) InsertOrIgnoreBegin(values [][]interface{}, keys []string) (int, []int64, error) { + if s.TableName == "" { + return 0, nil, errors.New("错误:未指定要操作的表名") + } + var placeholders []string + // 遍历数据 + for range keys { + placeholders = append(placeholders, "?") + } + + // 启动事务 + s.Tx, s.TxErr = s.Conn.Begin() + if s.TxErr != nil { + return 0, nil, s.TxErr + } + + // 清空参数 + defer s.clearOpt() + + // 预编译SQL语句 + s.Sql = "INSERT OR IGNORE INTO " + s.TableName + " (" + strings.Join(keys, ",") + ") VALUES (" + strings.Join(placeholders, ",") + ")" + stmt, err := s.Tx.Prepare(s.Sql) + if err != nil { + return 0, nil, err + } + defer func(stmt *sql.Stmt) { + err := stmt.Close() + if err != nil { + println("结束报错", err.Error()) + } + }(stmt) + + // 执行SQL语句 + insertIds := make([]int64, 0, len(values)) + ok := 0 + for _, val := range values { + tmp, err := stmt.Exec(val...) + if err != nil { + // 显示回滚任务, 避免数据库事务未完成导致资源占 + err2 := s.Tx.Rollback() + s.Tx = nil + if err2 != nil { + fmt.Println("回滚报错", err2.Error()) + } + return 0, nil, err + } + tmpId, _ := tmp.LastInsertId() + insertIds = append(insertIds, tmpId) + ok++ + } + + // 提交事务 + err = s.Tx.Commit() + s.Tx = nil + + return ok, insertIds, err +} + +/** + * @brief 使用事务批量插入更新数据 + * @param values [][]interface{} 要插入的数据 二维数组,每个数组为一条数据,数组中的值为字段值,顺序与keys对应 + * @param keys []string 字段名 + * @param uniqueIdx 依据的唯一索引的字段信息,如果数据库中不存在该索引,则会出现报错 如:(date, uri_id) + * @param updateFormula 更新字段的表达式,如: request = request + excluded.request + * @return int 影响的行数 + * @return error 错误信息 + */ +func (s *Sqlite) UpsertBegin(values [][]interface{}, keys []string, uniqueIdx []string, updateFormula string) (int, error) { + if s.TableName == "" { + return 0, errors.New("错误:未指定要操作的表名") + } + placeholdersBuilder := strings.Builder{} + var first = true + for range keys { + if first { + placeholdersBuilder.Write([]byte("?")) + first = false + } else { + placeholdersBuilder.Write([]byte(",?")) + } + } + placeholders := placeholdersBuilder.String() + + // 启动事务 + s.Tx, s.TxErr = s.Conn.Begin() + if s.TxErr != nil { + return 0, s.TxErr + } + + // 清空参数 + defer s.clearOpt() + + // 预编译SQL语句 + s.Sql = fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s", + s.TableName, strings.Join(keys, ","), placeholders, strings.Join(uniqueIdx, ","), updateFormula) + + stmt, err := s.Tx.Prepare(s.Sql) + if err != nil { + return 0, err + } + defer func(stmt *sql.Stmt) { + err := stmt.Close() + if err != nil { + println("结束报错", err.Error()) + } + }(stmt) + + // 执行SQL语句 + ok := 0 + for _, val := range values { + _, err = stmt.Exec(val...) + if err != nil { + // 显示回滚任务, 避免数据库事务未完成导致资源占 + err2 := s.Tx.Rollback() + s.Tx = nil + if err2 != nil { + fmt.Println("回滚报错", err2.Error()) + } + return 0, err + } + ok++ + } + + // 提交事务 + err = s.Tx.Commit() + s.Tx = nil + + return ok, err +} + +/** + * @brief 回滚事务 + * @return error 错误信息 + */ +func (s *Sqlite) Rollback() error { + err := s.Tx.Rollback() + s.Tx = nil + return err +} + +/** + * @brief 提交事务 + * @return error 错误信息 + */ +func (s *Sqlite) Commit() error { + err := s.Tx.Commit() + s.Tx = nil + return err +} + +/** + * @brief 设置JOIN查询 + * @param joinType string JOIN类型 + * @param table string JOIN表名 + * @param on string JOIN条件 + * @param param []interface{} JOIN参数 + * @return *Sqlite + */ +func (s *Sqlite) Join(joinType string, table string, on string, param []interface{}) *Sqlite { + joinType = strings.ToUpper(joinType) + if joinType != "LEFT" && joinType != "RIGHT" && joinType != "INNER" && joinType != "OUTER" { + joinType = "LEFT" + } + + if s.JoinTable == nil { + s.JoinTable = make([]string, 0) + } + s.JoinTable = append(s.JoinTable, s.PreFix+table) + + if s.JoinOn == nil { + s.JoinOn = make([]string, 0) + } + s.JoinOn = append(s.JoinOn, on) + + if s.JoinType == nil { + s.JoinType = make([]string, 0) + } + s.JoinType = append(s.JoinType, joinType) + + if s.JoinParam == nil { + s.JoinParam = make([]interface{}, 0) + } + + if len(param) > 0 { + if param[0] != nil { + s.JoinParam = append(s.JoinParam, param...) + } + } + + return s +} + +/** + * @brief 设置GROUP BY + * @param groupBy string GROUP BY + * @return *Sqlite + */ +func (s *Sqlite) GroupBy(groupBy string) *Sqlite { + s.OptGroupBy = " GROUP BY " + groupBy + return s +} + +/** + * @brief 设置HAVING + * @param having string HAVING + * @return *Sqlite + */ +func (s *Sqlite) Having(having string) *Sqlite { + s.OptHaving = " HAVING " + having + return s +} + +// /** +// * @brief 设置Group分组 +// * @param group string GROUP +// * @return *Sqlite +// */ +// func (s *Sqlite) Group(group string) *Sqlite { +// s.OptGroupBy = " " + group +// return s +// } + +/** + * @brief 使用SQL语句查询 + * @param sql string SQL语句 + * @param param []interface{} 绑定参数 + * @return []map[string]interface{} 数据集 + * @return error 错误信息 + */ +func (s *Sqlite) Query(sql string, param ...interface{}) ([]map[string]interface{}, error) { + + // 检查参数数量 + err := s.checkParam(s.Sql, s.OptParam) + if err != nil { + return nil, err + } + + // 执行SQL语句 + rows, err := s.Conn.Query(sql, param...) + if err != nil { + return nil, err + } + + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + return nil, err + } + + count := len(columns) + values := make([]interface{}, count) + valuePtrs := make([]interface{}, count) + + var result []map[string]interface{} + for rows.Next() { + for i := 0; i < count; i++ { + valuePtrs[i] = &values[i] + } + + rows.Scan(valuePtrs...) + + row := make(map[string]interface{}) + for i, col := range columns { + var v interface{} + val := values[i] + + b, ok := val.([]byte) + if ok { + v = string(b) + } else { + v = val + } + + row[col] = v + } + + result = append(result, row) + } + + return result, nil +} + +/** + * @brief 执行SQL语句 + * @param sql string SQL语句 + * @param param []interface{} 绑定参数 + * @return int64 影响的行数 + * @return error 错误信息 + */ +func (s *Sqlite) Exec(sql string, param ...interface{}) (int64, error) { + // 检查参数数量 + err := s.checkParam(s.Sql, s.OptParam) + if err != nil { + return 0, err + } + // 预编译SQL语句 + stmt, err := s.Conn.Prepare(sql) + if err != nil { + return 0, err + } + defer stmt.Close() + + // 执行SQL语句 + res, err := stmt.Exec(param...) + if err != nil { + return 0, err + } + + // 返回影响的行数 + return res.RowsAffected() +} + +/** + * @brief 清空查询条件 + * @return void + */ +func (s *Sqlite) clearOpt() { + s.OptField = nil + s.OptGroupBy = "" + s.OptWhere = "" + s.OptOrder = "" + s.OptLimit = "" + s.OptParam = nil + s.JoinOn = nil + s.JoinTable = nil + s.JoinType = nil + s.JoinParam = nil + s.Sql = "" +} + +func (s *Sqlite) IsClosed() bool { + return s.closed +} diff --git a/backend/public/utils.go b/backend/public/utils.go new file mode 100644 index 0000000..d318ccc --- /dev/null +++ b/backend/public/utils.go @@ -0,0 +1,181 @@ +package public + +import ( + "crypto/rand" + "fmt" + "github.com/google/uuid" + "io" + "math/big" + "net" + "net/http" + "strings" +) + +const defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +// GetSettingIgnoreError 获取系统配置-忽略错误 +func GetSettingIgnoreError(key string) string { + s, err := NewSqlite("data/data.db", "") + if err != nil { + return "" + } + s.Connect() + defer s.Close() + s.TableName = "settings" + res, err := s.Where("key=?", []interface{}{key}).Select() + if err != nil { + return "" + } + if len(res) == 0 { + return "" + } + setting, ok := res[0]["value"].(string) + if !ok { + return "" + } + return setting +} + +func UpdateSetting(key, val string) error { + s, err := NewSqlite("data/data.db", "") + if err != nil { + return err + } + s.Connect() + defer s.Close() + s.TableName = "settings" + _, err = s.Where("key=?", []interface{}{key}).Update(map[string]any{"value": val}) + if err != nil { + return err + } + return nil +} + +func GetSettingsFromType(typ string) ([]map[string]any, error) { + db := "data/data.db" + s, err := NewSqlite(db, "") + if err != nil { + return nil, err + } + s.Connect() + defer s.Close() + s.TableName = "settings" + res, err := s.Where("type=?", []interface{}{typ}).Select() + if err != nil { + return nil, err + } + + return res, nil +} + +// GetFreePort 获取一个可用的随机端口 +func GetFreePort() (int, error) { + // 端口为 0,表示让系统自动分配一个可用端口 + ln, err := net.Listen("tcp", "localhost:0") + if err != nil { + return 0, err + } + defer ln.Close() + + addr := ln.Addr().String() + // 提取端口号 + parts := strings.Split(addr, ":") + if len(parts) < 2 { + return 0, fmt.Errorf("invalid address: %s", addr) + } + + var port int + fmt.Sscanf(parts[len(parts)-1], "%d", &port) + return port, nil +} + +// RandomString 生成指定长度的随机字符串 +func RandomString(length int) string { + if str, err := RandomStringWithCharset(length, defaultCharset); err != nil { + return "allinssl" + } else { + return str + } +} + +// RandomStringWithCharset 使用指定字符集生成随机字符串 +func RandomStringWithCharset(length int, charset string) (string, error) { + result := make([]byte, length) + charsetLen := big.NewInt(int64(len(charset))) + + for i := 0; i < length; i++ { + num, err := rand.Int(rand.Reader, charsetLen) + if err != nil { + return "", err + } + result[i] = charset[num.Int64()] + } + + return string(result), nil +} + +// GenerateUUID 生成 UUID +func GenerateUUID() string { + // 生成一个新的 UUID + uuidStr := strings.ReplaceAll(uuid.New().String(), "-", "") + + // 返回 UUID 的字符串表示 + return uuidStr +} + +func GetLocalIP() (string, error) { + interfaces, err := net.Interfaces() + if err != nil { + return "", err + } + + for _, iface := range interfaces { + if iface.Flags&net.FlagUp == 0 { + continue // 接口未启用 + } + if iface.Flags&net.FlagLoopback != 0 { + continue // 忽略回环地址 + } + + addrs, err := iface.Addrs() + if err != nil { + continue + } + + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + + // 只返回 IPv4 内网地址 + if ip != nil && ip.To4() != nil && !ip.IsLoopback() { + return ip.String(), nil + } + } + } + + return "", fmt.Errorf("没有找到内网 IP") +} + +func GetPublicIP() (string, error) { + resp, err := http.Get("https://www.bt.cn/Api/getIpAddress") + if err != nil { + return "", fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP状态错误: %v", resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取响应失败: %v", err) + } + + return string(body), nil +} diff --git a/backend/public/validate_code.go b/backend/public/validate_code.go new file mode 100644 index 0000000..e6e3f1d --- /dev/null +++ b/backend/public/validate_code.go @@ -0,0 +1,35 @@ +package public + +import ( + "github.com/mojocn/base64Captcha" + "image/color" +) + +var codeDefaultDriver = base64Captcha.NewDriverString( + 1000, + 1200, + 0, + base64Captcha.OptionShowSlimeLine, + 4, + "23456789abcdefghjkmnpqrstuvwxyz", + &color.RGBA{ + R: 225, + G: 225, + B: 200, + A: 255, + }, + nil, + []string{"wqy-microhei.ttc", "RitaSmith.ttf"}, +) + +// GenerateCode 生成图形化字符串验证码 +func GenerateCode() (string, string, string, error) { + // 生成默认数字的driver + codeId, content, _ := codeDefaultDriver.GenerateIdQuestionAnswer() // 生成验证码和随机id + item, err := codeDefaultDriver.DrawCaptcha(content) // 生成验证码图片 + if err != nil { + return "", "", "", err + } + b64s := item.EncodeB64string() + return codeId, b64s, content, nil +} diff --git a/backend/route/route.go b/backend/route/route.go new file mode 100644 index 0000000..d5f29a3 --- /dev/null +++ b/backend/route/route.go @@ -0,0 +1,86 @@ +package route + +import ( + "ALLinSSL/backend/app/api" + "github.com/gin-gonic/gin" + "net/http" +) + +func Register(r *gin.Engine) { + v1 := r.Group("/v1") + + login := v1.Group("/login") + { + login.POST("/sign", api.Sign) + login.POST("/sign-out", api.SignOut) + login.GET("/get_code", api.GetCode) + } + siteMonitor := v1.Group("/siteMonitor") + { + siteMonitor.POST("/get_list", api.GetMonitorList) + siteMonitor.POST("/add_site_monitor", api.AddMonitor) + siteMonitor.POST("/upd_site_monitor", api.UpdMonitor) + siteMonitor.POST("/del_site_monitor", api.DelMonitor) + siteMonitor.POST("/set_site_monitor", api.SetMonitor) + } + workflow := v1.Group("/workflow") + { + workflow.POST("/get_list", api.GetWorkflowList) + workflow.POST("/add_workflow", api.AddWorkflow) + workflow.POST("/del_workflow", api.DelWorkflow) + workflow.POST("/upd_workflow", api.UpdWorkflow) + workflow.POST("/exec_type", api.UpdExecType) + workflow.POST("/active", api.UpdActive) + workflow.POST("/execute_workflow", api.ExecuteWorkflow) + workflow.POST("/get_workflow_history", api.GetWorkflowHistory) + workflow.POST("/get_exec_log", api.GetExecLog) + workflow.POST("/stop", api.StopWorkflow) + } + access := v1.Group("/access") + { + access.POST("/get_list", api.GetAccessList) + access.POST("/add_access", api.AddAccess) + access.POST("/del_access", api.DelAccess) + access.POST("/upd_access", api.UpdateAccess) + access.POST("/get_all", api.GetAllAccess) + } + cert := v1.Group("/cert") + { + cert.POST("/get_list", api.GetCertList) + cert.POST("/upload_cert", api.UploadCert) + cert.POST("/del_cert", api.DelCert) + cert.GET("/download", api.DownloadCert) + } + report := v1.Group("/report") + { + report.POST("/get_list", api.GetReportList) + report.POST("/add_report", api.AddReport) + report.POST("/del_report", api.DelReport) + report.POST("/upd_report", api.UpdReport) + report.POST("/notify_test", api.NotifyTest) + } + setting := v1.Group("/setting") + { + setting.POST("/get_setting", api.GetSetting) + setting.POST("/save_setting", api.SaveSetting) + setting.POST("/shutdown", api.Shutdown) + setting.POST("/restart", api.Restart) + } + overview := v1.Group("/overview") + { + overview.POST("/get_overviews", api.GetOverview) + } + + // 1. 提供静态文件服务 + r.StaticFS("/static", http.Dir("./frontend/static")) // 静态资源路径 + r.StaticFS("/auto-deploy/static", http.Dir("./frontend/static")) // 静态资源路径 + + // 3. 前端路由托管:匹配所有其他路由并返回 index.html + r.NoRoute(func(c *gin.Context) { + c.File("./frontend/index.html") + }) + // v2 := r.Group("/v2") + // { + // v2.POST("/submit") + // } +} diff --git a/backend/scheduler/scheduler.go b/backend/scheduler/scheduler.go new file mode 100644 index 0000000..398d0db --- /dev/null +++ b/backend/scheduler/scheduler.go @@ -0,0 +1,129 @@ +package scheduler + +import ( + "context" + "sync" + "time" +) + +// 你的任务列表 +var funcs = []func(){ + SiteMonitor, + RunWorkflows, +} + +// Scheduler 控制器 +type Scheduler struct { + mu sync.Mutex + ctx context.Context + cancelFunc context.CancelFunc + running bool + wg sync.WaitGroup +} + +// 启动调度器(在 goroutine 中运行) +func (s *Scheduler) Start() { + s.mu.Lock() + defer s.mu.Unlock() + + if s.running { + return + } + + s.ctx, s.cancelFunc = context.WithCancel(context.Background()) + s.running = true + s.wg.Add(1) + + go s.loop() // goroutine 中运行任务调度 +} + +// 停止调度器 +func (s *Scheduler) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.running { + return + } + + s.cancelFunc() // 取消上下文 + s.wg.Wait() // 等待 goroutine 完成退出 + s.running = false // 标记为未运行 +} + +// 重启调度器 +func (s *Scheduler) Restart() { + s.Stop() + time.Sleep(500 * time.Millisecond) // 可选,避免 race + s.Start() +} + +// 调度主循环(内部) +func (s *Scheduler) loop() { + defer s.wg.Done() + + for { + // fmt.Println("Scheduler loop") + select { + case <-s.ctx.Done(): + return // 外部关闭信号,退出 + default: + start := time.Now() + + var taskWg sync.WaitGroup + taskWg.Add(len(funcs)) + + for _, f := range funcs { + go func(fn func()) { + defer taskWg.Done() + fn() + }(f) + } + taskWg.Wait() + + // 间隔控制 + elapsed := time.Since(start) + if elapsed < 10*time.Second { + select { + case <-time.After(10*time.Second - elapsed): + case <-s.ctx.Done(): + return + } + } + } + } +} + +// package scheduler +// +// import ( +// "sync" +// "time" +// ) +// +// var funcs = []func(){ +// SiteMonitor, +// RunWorkflows, +// } +// +// func Scheduler() { +// for { +// start := time.Now() +// +// var wg sync.WaitGroup +// wg.Add(len(funcs)) +// +// for _, f := range funcs { +// go func(fn func()) { +// defer wg.Done() +// fn() +// }(f) +// } +// wg.Wait() +// // 保证每轮间隔至少10秒 +// elapsed := time.Since(start) +// if elapsed < 10*time.Second { +// time.Sleep(10*time.Second - elapsed) +// } +// } +// } diff --git a/backend/scheduler/site_monitor.go b/backend/scheduler/site_monitor.go new file mode 100644 index 0000000..ab73804 --- /dev/null +++ b/backend/scheduler/site_monitor.go @@ -0,0 +1,90 @@ +package scheduler + +import ( + "ALLinSSL/backend/internal/report" + "ALLinSSL/backend/internal/siteMonitor" + "fmt" + "os" + "path/filepath" + "strconv" + "sync" + "time" +) + +func SiteMonitor() { + s, err := siteMonitor.GetSqlite() + if err != nil { + fmt.Println(err) + } + defer s.Close() + data, err := s.Select() + if err != nil { + fmt.Println(err) + } + now := time.Now() + loc := now.Location() + var wg sync.WaitGroup + for _, v := range data { + if v["active"].(int64) == 1 { + lastTimeStr := v["last_time"].(string) + lastTime, err := time.ParseInLocation("2006-01-02 15:04:05", lastTimeStr, loc) + if err != nil { + // fmt.Println(err) + continue + } + if now.Sub(lastTime).Minutes() >= float64(v["cycle"].(int64)) { + wg.Add(1) + go func() { + defer wg.Done() + Err := siteMonitor.UpdInfo(fmt.Sprintf("%d", v["id"].(int64)), v["site_domain"].(string), s, v["report_type"].(string)) + + path := fmt.Sprintf("data/site_monitor/%d", v["id"].(int64)) + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return + } + errCount := 0 + file, err := os.ReadFile(path) + if err != nil { + errCount = 0 + } + errCount, err = strconv.Atoi(string(file)) + if err != nil { + errCount = 0 + } + + // 此处应该发送错误邮件 + if Err != nil { + errCount += 1 + os.WriteFile(path, []byte(strconv.Itoa(errCount)), os.ModePerm) + repeatSendGap, ok := v["repeat_send_gap"].(int64) + if !ok { + repeatSendGap = 10 + } + reportType, ok := v["report_type"].(string) + if ok && errCount >= int(repeatSendGap) { + s.TableName = "report" + rdata, err := s.Where("type=?", []interface{}{reportType}).Select() + if err != nil { + return + } + if len(rdata) <= 0 { + return + } + _ = report.Notify(map[string]any{ + "provider": reportType, + "provider_id": strconv.FormatInt(rdata[0]["id"].(int64), 10), + "body": fmt.Sprintf("检测到域名为%s的网站出现异常,请保持关注!\n检测时间:%s", v["site_domain"].(string), now.Format("2006-01-02 15:04:05")), + "subject": "ALLinSSL网站监控通知", + }) + os.Remove(path) + } + } else { + os.Remove(path) + } + }() + wg.Wait() + } + } + } +} diff --git a/backend/scheduler/workflow.go b/backend/scheduler/workflow.go new file mode 100644 index 0000000..93e52f9 --- /dev/null +++ b/backend/scheduler/workflow.go @@ -0,0 +1,104 @@ +package scheduler + +import ( + wf "ALLinSSL/backend/internal/workflow" + "encoding/json" + "fmt" + "strconv" + "sync" + "time" +) + +type ExecTime struct { + Type string `json:"type"` // "day", "week", "month" + Month int `json:"month,omitempty"` // 每月几号 type="month"时必填 + Week int `json:"week,omitempty"` // 星期几 type="week"时必填 + Hour int `json:"hour"` // 几点 必填 + Minute int `json:"minute"` // 几分 必填 +} + +func RunWorkflows() { + s, err := wf.GetSqlite() + if err != nil { + fmt.Println(err) + return + } + defer s.Close() + data, err := s.Select() + if err != nil { + fmt.Println(err) + return + } + now := time.Now() + // 遍历工作流 + var wg sync.WaitGroup + for _, workflow := range data { + if workflow["exec_type"].(string) != "auto" { + // fmt.Println("不是自动执行的工作流") + continue + } + if workflow["active"].(int64) == 0 { + // 1: 启用 + // 0: 禁用 + // fmt.Println("工作流未启用") + continue + } + if workflow["last_run_status"] != nil && workflow["last_run_status"].(string) == "running" { + // fmt.Println("工作流正在运行") + continue + } + // if workflow["last"] + if workflow["last_run_time"] != nil && now.Format("2006-01-02 15:04") == workflow["last_run_time"].(string)[0:16] { + // fmt.Println("工作流已执行过") + continue + } + // 判断是否到执行时间 + var execTime ExecTime + execTimeStr := "" + if et, ok := workflow["exec_time"].(string); ok { + execTimeStr = et + } + err := json.Unmarshal([]byte(execTimeStr), &execTime) + if err != nil { + // fmt.Println("解析执行时间失败:", err) + continue + } + if execTime.Minute != now.Minute() || execTime.Hour != now.Hour() { + // fmt.Println("不在执行时间内1") + continue + } + + if execTime.Type == "week" && execTime.Week != int(now.Weekday()) { + // fmt.Println("不在执行时间内2") + continue + } + if execTime.Type == "month" && execTime.Month != now.Day() { + // fmt.Println("不在执行时间内3") + continue + } + if content, ok := workflow["content"].(string); !ok { + // fmt.Println("工作流内容为空") + continue + } else { + wg.Add(1) + go func(id int64, c string) { + defer wg.Done() + WorkflowID := strconv.FormatInt(id, 10) + RunID, err := wf.AddWorkflowHistory(WorkflowID, "auto") + if err != nil { + return + } + ctx := wf.NewExecutionContext(RunID) + defer ctx.Logger.Close() + err = wf.RunWorkflow(c, ctx) + if err != nil { + fmt.Println("执行工作流失败:", err) + wf.SetWorkflowStatus(WorkflowID, RunID, "fail") + } else { + wf.SetWorkflowStatus(WorkflowID, RunID, "success") + } + }(workflow["id"].(int64), content) + } + } + wg.Wait() +} diff --git a/backend/server/server.go b/backend/server/server.go new file mode 100644 index 0000000..8dec339 --- /dev/null +++ b/backend/server/server.go @@ -0,0 +1,76 @@ +package server + +import ( + "ALLinSSL/backend/middleware" + "ALLinSSL/backend/public" + "ALLinSSL/backend/route" + "context" + "crypto/tls" + "encoding/gob" + "github.com/gin-contrib/gzip" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/memstore" + "github.com/gin-gonic/gin" + "net/http" + "time" +) + +func Run() error { + public.ReloadConfig() + public.InitLogger(public.LogPath) + defer public.CloseLogger() + r := gin.Default() + + store := memstore.NewStore([]byte("secret")) // 只在内存中,不持久化 + r.Use(sessions.Sessions(public.SessionKey, store)) + r.Use(middleware.LoggerMiddleware()) + r.Use(gzip.Gzip(gzip.DefaultCompression)) + gob.Register(time.Time{}) + r.Use(middleware.SessionAuthMiddleware()) + // r.Use(middleware.OpLoggerMiddleware()) + route.Register(r) + ctx, cancel := context.WithCancel(context.Background()) + public.ShutdownFunc = cancel + err := RunServer(ctx, r) + if err != nil { + return err + } + return nil +} + +func RunServer(ctx context.Context, r *gin.Engine) error { + + // 初始化http服务 + srv := &http.Server{ + Addr: ":" + public.Port, + Handler: r, + } + errchan := make(chan error, 1) + if public.GetSettingIgnoreError("https") == "1" { + srv.TLSConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, // 推荐设置最低 TLS 版本 + } + go func() { + defer close(errchan) + err := srv.ListenAndServeTLS("data/https/cert.pem", "data/https/key.pem") + if err != nil { + errchan <- err + } + }() + } else { + go func() { + defer close(errchan) + err := srv.ListenAndServe() + if err != nil { + errchan <- err + } + }() + } + select { + case err := <-errchan: + return err + case <-ctx.Done(): + _ = srv.Shutdown(ctx) + } + return nil +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..221da37 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,435 @@ +package main + +import ( + _ "ALLinSSL/backend/migrations" + "ALLinSSL/backend/public" + "ALLinSSL/backend/scheduler" + "ALLinSSL/backend/server" + "crypto/md5" + "encoding/hex" + "fmt" + "github.com/joho/godotenv" + ps "github.com/mitchellh/go-ps" + "os" + "os/exec" + "os/signal" + "runtime" + "strconv" + "syscall" +) + +var s = &scheduler.Scheduler{} +var pidPath = "data/pid" +var envPath = "data/.env" + +var envVars = map[string]string{ + "web": "start", + "scheduler": "start", +} + +func main() { + if len(os.Args) < 2 { + fmt.Println(`请不要直接运行本程序`) + // start() + return + } + env, err := godotenv.Read(envPath) + if err == nil { + envVars = env + } + + cmd := os.Args[1] + switch cmd { + case "start": + mainRun() + case "1": + fmt.Println("Starting ALLinSSL...") + if checkRun() { + _ = exec.Command("/bin/bash", "-c", fmt.Sprintf("nohup %s start > /dev/null 2>&1 &", os.Args[0])).Run() + fmt.Println("Started ALLinSSL") + return + } + fmt.Println("ALLinSSL is already running") + case "2": + fmt.Println("Stopping ALLinSSL...") + pid, err := os.ReadFile(pidPath) + if err != nil { + fmt.Println("Error reading pid file") + return + } + exec.Command("kill", "-9", string(pid)).Run() + os.Remove(pidPath) + fmt.Println("Stopped ALLinSSL") + case "3": + fmt.Println("Restarting ALLinSSL...") + pid, err := os.ReadFile(pidPath) + if err != nil { + fmt.Println("Error reading pid file") + } + exec.Command("kill", "-9", string(pid)).Run() + os.Remove(pidPath) + exec.Command("/bin/bash", "-c", fmt.Sprintf("nohup %s start > /dev/null 2>&1 &", os.Args[0])).Run() + fmt.Println("Restarted ALLinSSL") + case "4": + var secure string + if len(os.Args) > 2 { + secure = os.Args[2] + } else { + fmt.Print("请输入安全入口: ") + fmt.Scanln(&secure) + } + if len(secure) < 5 { + fmt.Println("安全入口至少需要5位") + return + } + if secure[0] != '/' { + secure = "/" + secure + } + err := public.UpdateSetting("secure", secure) + if err != nil { + fmt.Println("Error updating setting:", err) + return + } + envVars["web"] = "restart" + err = control() + fmt.Println("安全入口设置成功:", secure) + case "5": + var input string + if len(os.Args) > 2 { + input = os.Args[2] + } else { + fmt.Print("请输入用户名: ") + fmt.Scanln(&input) + } + if len(input) < 5 { + fmt.Println("用户名至少需要5位") + return + } + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + fmt.Println(err) + return + } + defer s.Close() + s.TableName = "users" + _, err = s.Where("id=1", []interface{}{}).Update(map[string]interface{}{"username": input}) + if err != nil { + fmt.Println(err) + return + } + public.UpdateSetting("login_key", public.GenerateUUID()) + fmt.Println("用户名设置成功:", input) + case "6": + var input string + if len(os.Args) > 2 { + input = os.Args[2] + } else { + fmt.Print("请输入密码: ") + fmt.Scanln(&input) + } + if len(input) < 8 { + fmt.Println("密码至少需要8位") + return + } + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + fmt.Println(err) + return + } + defer s.Close() + s.TableName = "users" + user, err := s.Where("id=1", []interface{}{}).Select() + if err != nil { + fmt.Println("Error selecting user:", err) + return + } + if len(user) == 0 { + fmt.Println("No user") + return + } + salt := user[0]["salt"].(string) + passwd := input + "_bt_all_in_ssl" + keyMd5 := md5.Sum([]byte(passwd)) + passwdMd5 := hex.EncodeToString(keyMd5[:]) + passwdMd5 += salt + keyMd5 = md5.Sum([]byte(passwdMd5)) + passwdMd5 = hex.EncodeToString(keyMd5[:]) + + _, err = s.Where("id=1", []interface{}{}).Update(map[string]interface{}{"password": passwdMd5}) + if err != nil { + fmt.Println(err) + return + } + public.UpdateSetting("login_key", public.GenerateUUID()) + + fmt.Println("密码设置成功:", input) + case "7": + var input string + if len(os.Args) > 2 { + input = os.Args[2] + } else { + fmt.Print("请输入端口号: ") + fmt.Scanln(&input) + } + port, err := strconv.Atoi(input) // 转换成整数 + if err != nil { + fmt.Println("端口号必须是数字!") + return + } + if port < 1 || port > 65535 { + fmt.Println("端口号必须在 1 到 65535 之间!") + return + } + err = public.UpdateSetting("port", input) + if err != nil { + fmt.Println("Error updating setting:", err) + return + } + fmt.Println("端口号设置成功:", input) + envVars["web"] = "restart" + control() + case "8": + envVars["web"] = "stop" + if control() != nil { + return + } + fmt.Println("web服务已停止") + case "9": + envVars["web"] = "start" + if control() != nil { + return + } + fmt.Println("web服务已启动") + case "10": + envVars["web"] = "restart" + if control() != nil { + return + } + fmt.Println("已重启web服务") + case "11": + envVars["scheduler"] = "stop" + if control() != nil { + return + } + fmt.Println("后台调度服务已停止") + case "12": + envVars["scheduler"] = "start" + if control() != nil { + return + } + fmt.Println("后台调度服务已开启") + + case "13": + envVars["scheduler"] = "restart" + if control() != nil { + return + } + fmt.Println("已重启后台调度服务") + + case "14": + err := public.UpdateSetting("https", "0") + if err != nil { + fmt.Println("Error updating setting:", err) + return + } + envVars["web"] = "restart" + control() + case "15": + public.ReloadConfig() + http := "http" + if public.GetSettingIgnoreError("https") == "1" { + http = "https" + } + + localIp, err := public.GetLocalIP() + if err != nil { + localIp = "0.0.0.0" + } + localAddr := fmt.Sprintf("%s://%s:%s%s", http, localIp, public.Port, public.Secure) + publicIp, err := public.GetPublicIP() + if err != nil { + publicIp = "0.0.0.0" + } + publicAddr := fmt.Sprintf("%s://%s:%s%s", http, publicIp, public.Port, public.Secure) + + s, err := public.NewSqlite("data/data.db", "") + if err != nil { + fmt.Println(err) + return + } + defer s.Close() + s.TableName = "users" + user, err := s.Where("id=1", []interface{}{}).Select() + if err != nil { + fmt.Println("Error selecting user:", err) + return + } + if len(user) == 0 { + fmt.Println("No user") + return + } + username, ok := user[0]["username"].(string) + if !ok { + fmt.Println("Error getting username") + return + } + + fmt.Printf(` + 外网面板地址: %s + 内网面板地址: %s + 用户名: %s + 密码: ******** +`, + publicAddr, localAddr, username) + default: + fmt.Println("无效的命令") + } +} + +func control() (err error) { + pidStr, err := os.ReadFile(pidPath) + if err != nil { + fmt.Println("Error reading pid file") + return + } + err = godotenv.Write(envVars, envPath) + 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 +} + +func mainRun() { + if checkRun() { + pid := os.Getpid() + os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", pid)), 0644) + defer os.Remove(pidPath) + go func() { + fmt.Println("web服务正在运行...") + err := server.Run() + if err != nil { + fmt.Println("web服务在运行时遇到错误:", err) + } + fmt.Println("web服务已停止") + }() + fmt.Println("正在启动调度器...") + go s.Start() + fmt.Println("调度器启动成功") + + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGHUP) + for { + <-c + run() + } + } else { + fmt.Println("服务已经启动") + } +} + +func checkRun() bool { + _, err := os.Stat(pidPath) + if err != nil { + return true + } + pid, err := os.ReadFile(pidPath) + if err != nil { + os.Remove(pidPath) + return true + } + pidInt, err := strconv.Atoi(string(pid)) + if err != nil { + fmt.Println("Error converting pid to int") + os.Remove(pidPath) + return true + } + switch runtime.GOOS { + case "windows": + return isProcessAliveWindows(pidInt) + default: + return isProcessAliveUnix(pidInt) + } +} + +// Unix/Linux/macOS 检测 +func isProcessAliveUnix(pid int) bool { + proc, err := os.FindProcess(pid) + if err != nil { + return true + } + // 发送 0 信号,不会杀死,只是检测 + err = proc.Signal(syscall.Signal(0)) + return err != nil +} + +// Windows 检测(遍历进程表) +func isProcessAliveWindows(pid int) bool { + processList, err := ps.Processes() + if err != nil { + return true + } + for _, p := range processList { + if p.Pid() == pid { + return false + } + } + return true +} + +func run() { + envVars, err := godotenv.Read(envPath) + if err != nil { + fmt.Println("Error reading .env file") + } + switch envVars["web"] { + case "start": + go func() { + fmt.Println("web服务正在运行...") + err := server.Run() + if err != nil { + fmt.Println("web服务在运行时遇到错误:", err) + return + // errchan <- err + } + fmt.Println("web服务已停止") + }() + case "stop": + public.ShutdownFunc() + fmt.Println("web服务已停止") + case "restart": + public.ShutdownFunc() + go func() { + fmt.Println("web服务正在运行...") + err := server.Run() + if err != nil { + fmt.Println("web服务在运行时遇到错误:", err) + return + } + fmt.Println("web服务已停止") + }() + } + switch envVars["scheduler"] { + case "start": + go s.Start() + case "stop": + s.Stop() + case "restart": + s.Restart() + } +} diff --git a/script/allinssl.sh b/script/allinssl.sh new file mode 100644 index 0000000..6f8c22b --- /dev/null +++ b/script/allinssl.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +# 设置工作目录 +WORK_DIR="/www/allinssl" + +# 检查工作目录是否存在 +if [ ! -d "$WORK_DIR" ]; then + echo "目录 $WORK_DIR 不存在,正在创建..." + mkdir -p "$WORK_DIR" +fi + +# 切换到工作目录 +cd "$WORK_DIR" || exit + +# 检查二进制文件是否存在 +BINARY_FILE="allinssl" +if [ ! -f "$BINARY_FILE" ]; then + echo "二进制文件 $BINARY_FILE 不存在,请确保已编译并放置在 $WORK_DIR 目录下。" + exit 1 +fi + +if [ $# -eq 0 ]; then + echo "=========== ALLinSSL 控制台 ===========" + echo "1: 启动服务" + echo "2: 停止服务" + echo "3: 重启服务" + echo "4: 修改安全入口" + echo "5: 修改用户名" + echo "6: 修改密码" + echo "7: 修改端口" + echo "8: 关闭web服务" + echo "9: 开启web服务" + echo "10: 重启web服务" + echo "11: 关闭后台自动调度" + echo "12: 开启后台自动调度" + echo "13: 重启后台自动调度" + echo "14: 关闭https" + echo "15: 获取面板地址" + echo "16: 更新ALLinSSL到最新版本(文件覆盖安装)" + echo "17: 卸载ALLinSSL" + echo "========================================" + read -p "请输入操作编号 (1-17): " user_input + + if [[ ! "$user_input" =~ ^([1-9]|1[0-7])$ ]]; then + echo "❌ 非法操作编号:$user_input" + exit 1 + fi + + set -- "$user_input" +fi + +function update_allinssl() { + local url="http://192.168.69.167:8888/down/Kuguq0edGNRA.tar.gz" + local target_dir="${WORK_DIR}" + local temp_file=$(mktemp) + local original_filename temp_file + # 创建目录 + create_directory() { + echo -e "${BLUE}${GEAR} Creating directory...${NC}" + ${SUDO} mkdir -p "$target_dir" || { + echo -e "${RED}${CROSS} Error: Failed to create directory $target_dir${NC}" + exit 1 + } + } + + # 下载文件 + download_file() { + echo -e "${BLUE}${DOWNLOAD} Downloading from $url...${NC}" + + # 获取原始文件名(去除URL参数) + original_filename=$(basename "$url" | cut -d '?' -f1) + [[ -z "$original_filename" ]] && { + echo -e "${RED}${CROSS} Error: Cannot determine filename from URL${NC}" + exit 1 + } + + temp_file="${temp_dir}/${original_filename}" + + wget --no-check-certificate -O "$temp_file" "$url" || { + echo -e "${RED}${CROSS} Error: Download failed${NC}" + exit 1 + } + + echo -e "${BLUE}⚙️ 保存文件名: ${original_filename}${NC}" + } + + # 解压文件 + extract_file() { + echo -e "${BLUE}${PACKAGE} Extracting to $target_dir...${NC}" + case "$temp_file" in + *.tar.gz|*.tgz) + ${SUDO} tar xzf "$temp_file" -C "$target_dir" + ;; + *.zip) + ${SUDO} unzip -q "$temp_file" -d "$target_dir" + ;; + *) + echo -e "${RED}${CROSS} 不支持的压缩格式: ${temp_file##*.}${NC}" + exit 1 + ;; + esac || { + echo -e "${RED}${CROSS} 解压失败,请检查文件完整性${NC}" + exit 1 + } + } + + set_cloudc() { + echo -e "${BLUE}${GEAR} Setting up ALLinSSL...${NC}" + chmod 755 "$target_dir/allinssl" + chmod +x "$target_dir/allinssl" + chmod 755 "$target_dir/allinssl.sh" + chmod +x "$target_dir/allinssl.sh" + ln -s "$target_dir/allinssl.sh" /usr/bin/allinssl + cd $target_dir || exit 1 + allinssl 3 + } + + # 清理临时文件 + cleanup() { + rm -f "$temp_file" + echo -e "${GREEN}${CLEAN} Temporary files cleaned${NC}" + } + + # 执行安装流程 + if create_directory && download_file && extract_file; then +# copy_config + set_cloudc + cleanup + echo -e "${GREEN}${CHECK} Successfully installed to $target_dir${NC}" + return 0 + else + cleanup + exit 1 + fi +} + +# 判断特殊操作 +if [ "$1" == "16" ]; then + echo "⚠️ 正在准备执行 ALLinSSL 更新操作..." + read -p "是否继续更新?(y/n): " confirm + if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then + echo "已取消更新操作。" + exit 0 + fi + + # 可在此插入更新逻辑(如下载新版、替换二进制等) + update_allinssl + echo "✅ 已确认,执行更新操作..." + exit 0 +elif [ "$1" == "17" ]; then + echo "⚠️ 正在准备执行 ALLinSSL 卸载操作..." + read -p "是否确认卸载 ALLinSSL?这将删除相关组件,此操作不可逆!(y/n): " confirm + if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then + echo "已取消卸载操作。" + exit 0 + fi + + # 可在此插入卸载逻辑(如删除文件、清除服务等) + echo "✅ 已确认,执行卸载操作..." + # 删除工作目录 + rm -rf "$WORK_DIR" + exit 0 +fi + +# 运行二进制文件 +"./$BINARY_FILE" "$@" \ No newline at end of file