diff --git a/plugins/kuocai/action.go b/plugins/kuocai/action.go new file mode 100644 index 0000000..9ae3b53 --- /dev/null +++ b/plugins/kuocai/action.go @@ -0,0 +1,165 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "math/rand" + "net/http" + "net/url" + "strings" + "time" +) + +type LoginInfo struct { + account string + password string +} +type LoginResponse struct { + Code string + Message string + Data string + Success bool +} + +type HTTPSConfig struct { + HTTPSStatus string `json:"https_status"` + CertificateSource string `json:"certificate_source"` + CertificateName string `json:"certificate_name"` + CertificateValue string `json:"certificate_value"` + PrivateKey string `json:"private_key"` +} + +type DeployConfig struct { + DomainID string `json:"doMainId"` + HTTPS HTTPSConfig `json:"https"` +} + +func randomString(n int) string { + rand.Seed(time.Now().UnixNano()) + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, n) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} + +const baseUrl = "https://www.kuocaiyun.com/" + +func login(loginInfo LoginInfo) (string, error) { + loginUrl := baseUrl + "login/loginUser" + formData := url.Values{} + formData.Set("userAccount", loginInfo.account) + formData.Set("userPwd", loginInfo.password) + formData.Set("remember", "true") + resp, err := http.Post(loginUrl, "application/x-www-form-urlencoded", strings.NewReader(formData.Encode())) + const emptyString = "" + if err != nil { + outputError("请求失败", err) + return emptyString, err + } + if resp.StatusCode != 200 { + return emptyString, errors.New("请求失败:" + string(rune(resp.StatusCode))) + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + outputError("关闭响应流失败", err) + } + }(resp.Body) + // 读取响应体 + var loginResp LoginResponse + if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil { + outputError("解析登录请求结果失败", err) + return emptyString, err + } + if !loginResp.Success || loginResp.Data == "" { + return emptyString, errors.New("登录请求失败:" + loginResp.Message) + } + return loginResp.Data, nil +} + +func deployCert(token string, domainId string, certKey string, certValue string) (map[string]interface{}, error) { + deployUrl := baseUrl + "CdnDomainHttps/httpsConfiguration" + params := DeployConfig{ + DomainID: domainId, + HTTPS: HTTPSConfig{ + HTTPSStatus: "on", + CertificateSource: "0", + CertificateName: "cert_" + randomString(13), + CertificateValue: certValue, + PrivateKey: certKey, + }, + } + // 序列化参数 + jsonData, err := json.Marshal(params) + if err != nil { + return nil, fmt.Errorf("JSON序列化失败: %v", err) + } + + req, err := http.NewRequest("POST", deployUrl, strings.NewReader(string(jsonData))) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Cookie", "kuocai_cdn_token="+token) + + client := &http.Client{} + resp, err := client.Do(req) + + if err != nil { + return nil, fmt.Errorf("HTTP请求失败: %v", err) + } + defer resp.Body.Close() + + // 解析响应 + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("响应解析失败: %v", err) + } + if !result["success"].(bool) { + return result, fmt.Errorf("更新证书失败") + } + return result, nil +} + +func Upload(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") + } + username, ok := cfg["username"].(string) + if !ok || username == "" { + return nil, fmt.Errorf("username is required and must be a string") + } + password, ok := cfg["password"].(string) + if !ok || password == "" { + return nil, fmt.Errorf("password is required and must be a string") + } + domainId, ok := cfg["domainId"].(string) + if !ok || domainId == "" { + return nil, fmt.Errorf("domainId is required and must be a string") + } + token, err := login(LoginInfo{account: username, password: password}) + if err != nil || token == "" { + return nil, fmt.Errorf("fetch token failed, err %v", err) + } + res, err := deployCert(token, domainId, keyStr, certStr) + if err != nil { + return nil, err + } + return &Response{ + Status: "success", + Message: "success", + Result: res, + }, nil +} diff --git a/plugins/kuocai/main.go b/plugins/kuocai/main.go new file mode 100644 index 0000000..9edb065 --- /dev/null +++ b/plugins/kuocai/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" +) + +type ActionInfo struct { + Name string `json:"name"` + Description string `json:"description"` + Params map[string]any `json:"params,omitempty"` +} + +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": "kuocai", + "description": "部署到括彩云", + "version": "1.0.0", + "author": "coclyun", + "config": map[string]interface{}{ + "username": "括彩云账号", + "password": "括彩云密码", + }, + "actions": []ActionInfo{ + { + Name: "upload", + Description: "部署到括彩云", + Params: map[string]any{}, + }, + }, +} + +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 "upload": + rep, err := Upload(req.Params) + if err != nil { + outputError("CDN 部署失败", err) + return + } + outputJSON(rep) + + default: + outputJSON(&Response{ + Status: "error", + Message: "未知 action: " + req.Action, + }) + } +}