Files
docker-compose-updater/cmd/dcu-send/main.go
T
ilovintit cea9b941cf
Build and Push / build (push) Failing after 13m20s
Initial commit: docker-compose-updater
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
2026-06-08 15:16:46 +08:00

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)
}