package main import ( "bytes" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "os" "time" "gitea.songhuwan.com/actions/docker-compose-updater/internal/auth" ) func main() { signingKeyPEM := os.Getenv("SIGNING_KEY") url := os.Getenv("UPDATER_URL") project := os.Getenv("UPDATER_PROJECT") service := os.Getenv("UPDATER_SERVICE") action := os.Getenv("UPDATER_ACTION") if signingKeyPEM == "" || url == "" || project == "" || service == "" || action == "" { fmt.Fprintln(os.Stderr, "required env: SIGNING_KEY, UPDATER_URL, UPDATER_PROJECT, UPDATER_SERVICE, UPDATER_ACTION") os.Exit(1) } if action != "update" && action != "pull" && action != "restart" { fmt.Fprintf(os.Stderr, "invalid action: %s (must be update/pull/restart)\n", action) os.Exit(1) } // 解析 ECDSA 私钥 signingKey, err := auth.ParseECDSAPrivateKey([]byte(signingKeyPEM)) if err != nil { fmt.Fprintf(os.Stderr, "parse signing key: %v\n", err) os.Exit(1) } client := &http.Client{Timeout: 30 * time.Second} // === Phase 1: 获取会话密钥 === nonce := randomBase64(16) ts := time.Now().Unix() signData := fmt.Sprintf("1.%d.%s", ts, nonce) sig, err := auth.Sign(signingKey, []byte(signData)) if err != nil { fmt.Fprintf(os.Stderr, "sign: %v\n", err) os.Exit(1) } phase1Body, _ := json.Marshal(map[string]any{ "v": 1, "ts": ts, "nonce": nonce, "sig": sig, }) baseURL := url resp, err := client.Post(baseURL+"/session", "application/json", bytes.NewReader(phase1Body)) if err != nil { fmt.Fprintf(os.Stderr, "phase1 request: %v\n", err) os.Exit(1) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { fmt.Fprintf(os.Stderr, "phase1 failed (%d): %s\n", resp.StatusCode, string(body)) os.Exit(1) } var phase1Resp struct { Key string `json:"key"` KeyID string `json:"key_id"` ExpiresIn int `json:"expires_in"` } if err := json.Unmarshal(body, &phase1Resp); err != nil { fmt.Fprintf(os.Stderr, "parse phase1 response: %v\n", err) os.Exit(1) } fmt.Fprintf(os.Stderr, "phase1 ok, key_id=%s\n", phase1Resp.KeyID) // === Phase 2: 发送加密请求 === payload, _ := json.Marshal(map[string]string{ "project": project, "service": service, "action": action, }) sessionKey, err := base64.StdEncoding.DecodeString(phase1Resp.Key) if err != nil { fmt.Fprintf(os.Stderr, "decode session key: %v\n", err) os.Exit(1) } ciphertext, err := auth.Encrypt(payload, sessionKey) if err != nil { fmt.Fprintf(os.Stderr, "encrypt: %v\n", err) os.Exit(1) } phase2Nonce := randomBase64(16) phase2Body, _ := json.Marshal(map[string]any{ "v": 1, "ts": time.Now().Unix(), "nonce": phase2Nonce, "key_id": phase1Resp.KeyID, "data": base64.StdEncoding.EncodeToString(ciphertext), }) resp, err = client.Post(baseURL+"/hook", "application/json", bytes.NewReader(phase2Body)) if err != nil { fmt.Fprintf(os.Stderr, "phase2 request: %v\n", err) os.Exit(1) } defer resp.Body.Close() body, _ = io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { fmt.Fprintf(os.Stderr, "phase2 failed (%d): %s\n", resp.StatusCode, string(body)) os.Exit(1) } // 输出响应(供 Action 日志查看) var result map[string]any json.Unmarshal(body, &result) fmt.Fprintf(os.Stderr, "result: %s\n", string(body)) // 打印关键信息到 stdout if msg, ok := result["message"].(string); ok { fmt.Println(msg) } } func randomBase64(n int) string { b := make([]byte, n) if _, err := rand.Read(b); err != nil { panic(err) } return base64.RawURLEncoding.EncodeToString(b) }