diff --git a/backend/internal/cert/deploy/deploy.go b/backend/internal/cert/deploy/deploy.go index 3395beb..6a26dea 100644 --- a/backend/internal/cert/deploy/deploy.go +++ b/backend/internal/cert/deploy/deploy.go @@ -1,6 +1,8 @@ package deploy import ( + "ALLinSSL/backend/internal/cert/deploy/doge" + "ALLinSSL/backend/internal/cert/deploy/plugin" "ALLinSSL/backend/public" "fmt" ) @@ -87,9 +89,12 @@ func Deploy(cfg map[string]any, logger *public.Logger) error { case "volcengine-dcdn": logger.Debug("部署到火山DCDN...") return DeployVolcEngineDCdn(cfg) - // case "plugin": - // logger.Debug("使用插件部署...") - // return DeployPlugin(cfg) + case "doge-cdn": + logger.Debug("部署到多吉云CDN...") + return doge.DeployCdn(cfg) + case "plugin": + logger.Debug("使用插件部署...") + return plugin.Deploy(cfg, logger) default: return fmt.Errorf("不支持的部署: %s", providerName) } diff --git a/backend/internal/cert/deploy/doge/deploy.go b/backend/internal/cert/deploy/doge/deploy.go new file mode 100644 index 0000000..1527a05 --- /dev/null +++ b/backend/internal/cert/deploy/doge/deploy.go @@ -0,0 +1,221 @@ +package doge + +import ( + "ALLinSSL/backend/public" + "crypto/hmac" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +type Auth struct { + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` +} + +func NewAuth(accessKey, secretKey string) *Auth { + return &Auth{ + AccessKey: accessKey, + SecretKey: secretKey, + } +} + +func DeployCdn(cfg map[string]any) error { + if cfg == nil { + return fmt.Errorf("config cannot be nil") + } + certStr, ok := cfg["cert"].(string) + if !ok || certStr == "" { + return fmt.Errorf("cert is required and must be a string") + } + keyStr, ok := cfg["key"].(string) + if !ok || keyStr == "" { + return fmt.Errorf("key is required and must be a string") + } + accessKey, ok := cfg["access_key"].(string) + if !ok || accessKey == "" { + return fmt.Errorf("access_key is required and must be a string") + } + secretKey, ok := cfg["secret_key"].(string) + if !ok || secretKey == "" { + return fmt.Errorf("secret_key is required and must be a string") + } + domain, ok := cfg["domain"].(string) + if !ok || domain == "" { + return fmt.Errorf("domain is required and must be a string") + } + sha256, err := public.GetSHA256(certStr) + if err != nil { + return fmt.Errorf("failed to get SHA256 of cert: %w", err) + } + note := fmt.Sprintf("allinssl-%s", sha256) + + a := NewAuth(accessKey, secretKey) + // 检查证书是否已存在于 CDN + certList, err := a.listCertFromCdn() + if err != nil { + return fmt.Errorf("failed to list certs from CDN: %w", err) + } + var certID float64 + for _, cert := range certList { + if cert["note"] == note { + certID, ok = cert["id"].(float64) + if !ok { + certID = 0 + } + } + } + // 如果证书不存在,则上传证书到 CDN + if certID == 0 { + certID, err = a.uploadCertToCdn(certStr, keyStr, note) + if err != nil || certID == 0 { + return fmt.Errorf("failed to upload to CDN: %w", err) + } + } + // 绑定证书到域名 + _, err = a.bindCertToCdn(certID, domain) + if err != nil { + return fmt.Errorf("failed to bind cert to CDN: %w", err) + } + + return nil +} + +func (a Auth) uploadCertToCdn(cert, key, note string) (float64, error) { + params := map[string]any{ + "cert": cert, + "private": key, + "note": note, + } + + res, err := a.DogeCloudAPI("/cdn/cert/upload.json", params, true) + if err != nil { + return 0, fmt.Errorf("failed to call DogeCloud API: %w", err) + } + code, ok := res["code"].(float64) + if !ok { + return 0, fmt.Errorf("invalid response format: code not found") + } + if code != 200 { + return 0, fmt.Errorf("DogeCloud API error: %s", res["msg"]) + } + data, ok := res["data"].(map[string]any) + if !ok { + return 0, fmt.Errorf("invalid response format: data not found") + } + certID, ok := data["id"].(float64) + if !ok { + return 0, fmt.Errorf("invalid response format: id not found") + } + return certID, nil +} + +func (a Auth) listCertFromCdn() ([]map[string]any, error) { + res, err := a.DogeCloudAPI("/cdn/cert/list.json", map[string]interface{}{}, true) + if err != nil { + return nil, fmt.Errorf("failed to call DogeCloud API: %w", err) + } + code, ok := res["code"].(float64) + if !ok { + return nil, fmt.Errorf("invalid response format: code not found") + } + if code != 200 { + return nil, fmt.Errorf("DogeCloud API error: %s", res["msg"]) + } + data, ok := res["data"].(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid response format: data not found") + } + certList, ok := data["certs"].([]any) + if !ok { + return nil, fmt.Errorf("invalid response format: certs not found") + } + certs := make([]map[string]any, 0, len(certList)) + for _, cert := range certList { + certMap, ok := cert.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid response format: cert item is not a map") + } + certs = append(certs, certMap) + } + return certs, nil +} + +func (a Auth) bindCertToCdn(certID float64, domain string) (map[string]interface{}, error) { + params := map[string]interface{}{ + "id": certID, + "domain": domain, + } + res, err := a.DogeCloudAPI("/cdn/cert/bind.json", params, true) + if err != nil { + return nil, fmt.Errorf("failed to call DogeCloud API: %w", err) + } + code, ok := res["code"].(float64) + if !ok { + return nil, fmt.Errorf("invalid response format: code not found") + } + if code != 200 { + return nil, fmt.Errorf("DogeCloud API error: %s", res["msg"]) + } + return res, nil + +} + +// DogeCloudAPI 调用多吉云的 API 根据多吉云官网示例修改 +func (a Auth) DogeCloudAPI(apiPath string, data map[string]interface{}, jsonMode bool) (map[string]interface{}, error) { + AccessKey := a.AccessKey + SecretKey := a.SecretKey + + body := "" + mime := "" + if jsonMode { + _body, err := json.Marshal(data) + if err != nil { + return nil, err + } + body = string(_body) + mime = "application/json" + } else { + values := url.Values{} + for k, v := range data { + values.Set(k, v.(string)) + } + body = values.Encode() + mime = "application/x-www-form-urlencoded" + } + + signStr := apiPath + "\n" + body + hmacObj := hmac.New(sha1.New, []byte(SecretKey)) + hmacObj.Write([]byte(signStr)) + sign := hex.EncodeToString(hmacObj.Sum(nil)) + Authorization := "TOKEN " + AccessKey + ":" + sign + + req, err := http.NewRequest("POST", "https://api.dogecloud.com"+apiPath, strings.NewReader(body)) + if err != nil { + return nil, err // 创建请求错误 + } + req.Header.Add("Content-Type", mime) + req.Header.Add("Authorization", Authorization) + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } // 网络错误 + defer resp.Body.Close() + r, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err // 读取响应错误 + } + var result map[string]interface{} + + err = json.Unmarshal(r, &result) + if err != nil { + return nil, err + } + return result, nil +} diff --git a/backend/internal/cert/deploy/doge/doge_test.go b/backend/internal/cert/deploy/doge/doge_test.go new file mode 100644 index 0000000..5926d3b --- /dev/null +++ b/backend/internal/cert/deploy/doge/doge_test.go @@ -0,0 +1,21 @@ +package doge + +import ( + "fmt" + "os" + "testing" +) + +func TestPlugin(t *testing.T) { + err := DeployCdn(map[string]interface{}{ + "access_key": "xxxxxx", + "secret_key": "xxxxx", + "key": "xxxxx", + "cert": "xxxxx", + "domain": "xx.com", + }) + if err != nil { + fmt.Fprintf(os.Stderr, "调用插件失败: %v\n", err) + os.Exit(1) + } +} diff --git a/backend/internal/cert/deploy/plugin/deploy.go b/backend/internal/cert/deploy/plugin/deploy.go index 767eedd..7dcb580 100644 --- a/backend/internal/cert/deploy/plugin/deploy.go +++ b/backend/internal/cert/deploy/plugin/deploy.go @@ -2,6 +2,7 @@ package plugin import ( "ALLinSSL/backend/internal/access" + "ALLinSSL/backend/public" "encoding/json" "fmt" "strconv" @@ -13,7 +14,7 @@ type CertDeployPlugin struct { Cert string } -func Deploy(cfg map[string]any) error { +func Deploy(cfg map[string]any, logger *public.Logger) error { cert, ok := cfg["certificate"].(map[string]any) if !ok { return fmt.Errorf("证书不存在") @@ -52,9 +53,9 @@ func Deploy(cfg map[string]any) error { if !ok { return fmt.Errorf("插件配置错误") } - pluginParams, ok := pluginConfig["params"].(string) + pluginParams, ok := cfg["params"].(string) if !ok { - return fmt.Errorf("插件参数错误:") + return fmt.Errorf("插件参数错误:params") } var paramsMap map[string]any err = json.Unmarshal([]byte(pluginParams), ¶msMap) @@ -76,12 +77,13 @@ func Deploy(cfg map[string]any) error { return fmt.Errorf("证书错误:cert") } - params := map[string]any{ - "config": pluginConfig, - "key": keyPem, - "cert": certPem, - } - rep, err := CallPlugin(pluginName, action, params) + pluginConfig["key"] = keyPem + pluginConfig["cert"] = certPem + + // 调用插件 + logger.Debug(fmt.Sprintf("调用插件%s:%s", pluginName, action)) + + rep, err := CallPlugin(pluginName, action, pluginConfig, logger) if err != nil { return fmt.Errorf("调用插件失败:%v", err) } diff --git a/backend/internal/cert/deploy/plugin/plugin.go b/backend/internal/cert/deploy/plugin/plugin.go index fd96fac..3361bbb 100644 --- a/backend/internal/cert/deploy/plugin/plugin.go +++ b/backend/internal/cert/deploy/plugin/plugin.go @@ -97,28 +97,29 @@ func getMetadata(path string) (PluginMetadata, error) { return meta, nil } -func CallPlugin(name, action string, params map[string]interface{}) (*Response, error) { +func CallPlugin(name, action string, params map[string]interface{}, logger *public.Logger) (*Response, error) { // 第一次尝试 - resp, err := tryCallPlugin(name, action, params) + resp, err := tryCallPlugin(name, action, params, logger) if err == nil { return resp, nil } // 如果是插件或 action 不存在,则刷新插件列表并再试一次 if errors.Is(err, ErrPluginNotFound) || errors.Is(err, ErrActionNotFound) { - fmt.Println("🔄 尝试刷新插件列表...") + logger.Debug("插件或插件内方法不存在,尝试刷新插件列表...") _, scanErr := scanPlugins("plugins") if scanErr != nil { + logger.Error("插件刷新失败", scanErr) return nil, fmt.Errorf("插件刷新失败: %v", scanErr) } - return tryCallPlugin(name, action, params) + return tryCallPlugin(name, action, params, logger) } // 其他错误直接返回 return nil, err } -func tryCallPlugin(name, action string, params map[string]interface{}) (*Response, error) { +func tryCallPlugin(name, action string, params map[string]interface{}, logger *public.Logger) (*Response, error) { plugin, ok := pluginRegistry[name] if !ok { return nil, ErrPluginNotFound @@ -133,6 +134,7 @@ func tryCallPlugin(name, action string, params map[string]interface{}) (*Respons } } if !found { + logger.Debug("插件不支持该 action", "plugin", name, "action", action) return nil, ErrActionNotFound } @@ -148,30 +150,37 @@ func tryCallPlugin(name, action string, params map[string]interface{}) (*Respons stdin, err := cmd.StdinPipe() if err != nil { + logger.Error("开启标准输入管道失败", err) return nil, err } stdout, err := cmd.StdoutPipe() if err != nil { + logger.Error("开启标准输出管道失败", err) return nil, err } if err := cmd.Start(); err != nil { + logger.Error("启动插件失败", err) return nil, err } if err := json.NewEncoder(stdin).Encode(req); err != nil { + logger.Error("发送插件请求失败", err) return nil, err } stdin.Close() respBytes, err := io.ReadAll(stdout) if err != nil { + logger.Error("读取插件响应失败", err) return nil, err } var resp Response if err := json.Unmarshal(respBytes, &resp); err != nil { + logger.Error("解析插件响应失败", err, "内容", string(respBytes)) return nil, fmt.Errorf("解析插件响应失败: %v\n内容: %s", err, respBytes) } cmd.Wait() + logger.Debug("插件响应", "plugin", name, "action", action, "response", resp) return &resp, nil } diff --git a/backend/public/logger.go b/backend/public/logger.go index df8c145..324b67e 100644 --- a/backend/public/logger.go +++ b/backend/public/logger.go @@ -1,9 +1,11 @@ package public import ( + "fmt" "log" "os" "path/filepath" + "strings" "sync" "time" ) @@ -89,27 +91,33 @@ func (l *Logger) Close() { } // write 写日志,内部使用锁保证线程安全 -func (l *Logger) write(level string, msg string) { +func (l *Logger) write(level string, args ...interface{}) { l.mutex.Lock() defer l.mutex.Unlock() timestamp := time.Now().Format("2006-01-02 15:04:05") - logLine := "[" + level + "] " + timestamp + " - " + msg + message := fmt.Sprintln(args...) // 自动拼接参数并换行 + logLine := "[" + level + "] " + timestamp + " - " + message + logLine = strings.TrimRight(logLine, "\n") // 去掉 Sprintln 自动加的换行 l.logger.Println(logLine) } // Info 输出 info 级别日志 -func (l *Logger) Info(msg string) { - l.write("INFO", msg) +func (l *Logger) Info(args ...interface{}) { + l.write("INFO", args...) } // Error 输出 error 级别日志 -func (l *Logger) Error(msg string) { - l.write("ERROR", msg) +func (l *Logger) Error(args ...interface{}) { + l.write("ERROR", args...) } -func (l *Logger) Debug(msg string) { - l.write("Debug", msg) + +// Debug 输出 debug 级别日志 +func (l *Logger) Debug(args ...interface{}) { + l.write("DEBUG", args...) } + +// 获取底层 logger 实例 func (l *Logger) GetLogger() *log.Logger { return l.logger }