From c5167b374e3746d8b5a5e1971e0bd8e512a1f6a0 Mon Sep 17 00:00:00 2001 From: zhangchenhao Date: Tue, 10 Jun 2025 16:22:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=AE=80=E5=8D=95=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E7=A4=BA=E4=BE=8B=EF=BC=88=E5=A4=9A=E5=90=89=E4=BA=91?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/doge/action.go | 225 +++++++++++++++++++++++++++++++++++++++++ plugins/doge/main.go | 106 +++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 plugins/doge/action.go create mode 100644 plugins/doge/main.go diff --git a/plugins/doge/action.go b/plugins/doge/action.go new file mode 100644 index 0000000..9c7fbd1 --- /dev/null +++ b/plugins/doge/action.go @@ -0,0 +1,225 @@ +package main + +import ( + "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 Cdn(cfg map[string]any) (*Response, error) { + if cfg == nil { + return nil, fmt.Errorf("config cannot be nil") + } + certStr, ok := cfg["cert"].(string) + if !ok || certStr == "" { + return nil, fmt.Errorf("cert is required and must be a string") + } + keyStr, ok := cfg["key"].(string) + if !ok || keyStr == "" { + return nil, fmt.Errorf("key is required and must be a string") + } + accessKey, ok := cfg["access_key"].(string) + if !ok || accessKey == "" { + return nil, fmt.Errorf("access_key is required and must be a string") + } + secretKey, ok := cfg["secret_key"].(string) + if !ok || secretKey == "" { + return nil, fmt.Errorf("secret_key is required and must be a string") + } + domain, ok := cfg["domain"].(string) + if !ok || domain == "" { + return nil, fmt.Errorf("domain is required and must be a string") + } + sha256, err := GetSHA256(certStr) + if err != nil { + return nil, fmt.Errorf("failed to get SHA256 of cert: %w", err) + } + note := fmt.Sprintf("allinssl-%s", sha256) + + a := NewAuth(accessKey, secretKey) + // 检查证书是否已存在于 CDN + // 只根据证书名称检查是否存在,格式为 "allinssl-" + certList, err := a.listCertFromCdn() + if err != nil { + return nil, 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 nil, fmt.Errorf("failed to upload to CDN: %w", err) + } + } + // 绑定证书到域名 + bindRes, err := a.bindCertToCdn(certID, domain) + if err != nil { + return nil, fmt.Errorf("failed to bind cert to CDN: %w", err) + } + + return &Response{ + Status: "success", + Message: "Certificate uploaded and bound successfully", + Result: bindRes, + }, 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/plugins/doge/main.go b/plugins/doge/main.go new file mode 100644 index 0000000..b005851 --- /dev/null +++ b/plugins/doge/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "os" +) + +type ActionInfo struct { + Name string `json:"name"` + Description string `json:"description"` +} + +type Request struct { + Action string `json:"action"` + Params map[string]interface{} `json:"params"` +} + +type Response struct { + Status string `json:"status"` + Message string `json:"message"` + Result map[string]interface{} `json:"result"` +} + +var pluginMeta = map[string]interface{}{ + "name": "doge", + "description": "部署到多吉云", + "version": "1.0.0", + "author": "主包", + "actions": []ActionInfo{ + {Name: "cdn", Description: "部署到多吉云cdn"}, + }, +} + +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 +} + +func outputJSON(resp *Response) { + _ = json.NewEncoder(os.Stdout).Encode(resp) +} + +func outputError(msg string, err error) { + outputJSON(&Response{ + Status: "error", + Message: fmt.Sprintf("%s: %v", msg, err), + }) +} + +func main() { + var req Request + input, err := io.ReadAll(os.Stdin) + if err != nil { + outputError("读取输入失败", err) + return + } + + if err := json.Unmarshal(input, &req); err != nil { + outputError("解析请求失败", err) + return + } + + switch req.Action { + case "get_metadata": + outputJSON(&Response{ + Status: "success", + Message: "插件信息", + Result: pluginMeta, + }) + case "list_actions": + outputJSON(&Response{ + Status: "success", + Message: "支持的动作", + Result: map[string]interface{}{"actions": pluginMeta["actions"]}, + }) + case "cdn": + rep, err := Cdn(req.Params) + if err != nil { + outputError("CDN 部署失败", err) + return + } + outputJSON(rep) + + default: + outputJSON(&Response{ + Status: "error", + Message: "未知 action: " + req.Action, + }) + } +}