cea9b941cf
Build and Push / build (push) Failing after 13m20s
Go 项目,包含: - 服务端 updater:两阶段协议,ECDSA 签名验证,AES-GCM 加密 - 发送端 dcu-send:Gitea Action CLI - internal/auth:加解密/签名/会话管理 - internal/docker:Docker CLI 容器查找/拉取/重建 - action/:Gitea Action 定义 - deploy/Dockerfile:多阶段构建 - .gitea/workflows/build.yaml:CI/CD
146 lines
3.6 KiB
Go
146 lines
3.6 KiB
Go
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)
|
|
}
|