Files
pangolin/infra/domains/tools/cmd/publish-mirrors/main.go
T
wangjia 7d89ec9d91 feat(infra/domains): 域名池 + CDN 前置 + 签名端点分发 (tsk_NU9JuUweHWMt)
- domains.md: 四组域名隔离登记 + 冷备池 ≥5 + 启用流程(不含身份信息)
- cdn/terraform: Cloudflare 配置即代码(WAF/bot/速率限制/代理DNS/回源鉴权注入)+ 30min 重放 Runbook
- server/internal/originauth: 回源鉴权中间件,非 CDN 网段或鉴权头不符一律 403,支持双值轮换
- tools/endpoint-signer: 离线 Ed25519 签名 CLI(端点 + 公告文档,单调版本防回滚,key_id 双公钥轮换)
- tools/publish-mirrors: ≥3 镜像发布 + hash 一致性校验 + 故障转移取回
- CLIENT-CONTRACT.md: schema/验签/防回滚/合并/兜底链/channel 客户端契约
- 出站独立出口要求写入部署文档;私钥/token/身份信息一律不入库

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:21:55 +08:00

172 lines
4.7 KiB
Go

// Command publish-mirrors pushes a signed distribution document to ≥3 mirrors
// and verifies every mirror serves byte-identical content (doc/05 §1/§5).
//
// Subcommands:
//
// publish -in doc.json -config mirrors.json [-name endpoints.v1.json] [-verify-only]
// fetch -config mirrors.json -name endpoints.v1.json [-keys "id=b64"] [-type endpoints|notices]
//
// The mirrors config lists each destination's local content root (dir) and an
// optional fetch_url used for read-back verification / failover fetch. The
// per-mirror sync_cmd (e.g. wrangler / aws s3 cp) is recorded for the operator
// but never executed here — publishing writes the object into dir, and the
// deploy ships dir to its platform out of band.
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"github.com/wangjia/pangolin/infra/domains/tools/internal/endpoint"
"github.com/wangjia/pangolin/infra/domains/tools/internal/mirror"
"github.com/wangjia/pangolin/infra/domains/tools/internal/sign"
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
var err error
switch os.Args[1] {
case "publish":
err = cmdPublish(os.Args[2:])
case "fetch":
err = cmdFetch(os.Args[2:])
case "-h", "--help", "help":
usage()
return
default:
fmt.Fprintf(os.Stderr, "unknown subcommand %q\n", os.Args[1])
usage()
os.Exit(2)
}
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
func usage() {
fmt.Fprint(os.Stderr, `publish-mirrors — push signed docs to ≥3 mirrors and verify consistency
publish -in doc.json -config mirrors.json [-name endpoints.v1.json] [-verify-only]
fetch -config mirrors.json -name endpoints.v1.json [-keys "id=b64"] [-type endpoints|notices]
`)
}
func loadConfig(path string) (mirror.Config, error) {
var cfg mirror.Config
raw, err := os.ReadFile(path)
if err != nil {
return cfg, err
}
if err := json.Unmarshal(raw, &cfg); err != nil {
return cfg, fmt.Errorf("parse mirrors config: %w", err)
}
if len(cfg.Mirrors) < 3 {
return cfg, fmt.Errorf("at least 3 mirrors required, got %d", len(cfg.Mirrors))
}
return cfg, nil
}
func cmdPublish(args []string) error {
fs := flag.NewFlagSet("publish", flag.ContinueOnError)
in := fs.String("in", "", "signed document to publish")
cfgPath := fs.String("config", "", "mirrors config JSON")
name := fs.String("name", "", "published object name (default: basename of -in)")
verifyOnly := fs.Bool("verify-only", false, "skip writing; only verify existing mirror content matches -in")
if err := fs.Parse(args); err != nil {
return err
}
if *in == "" || *cfgPath == "" {
return errors.New("-in and -config are required")
}
cfg, err := loadConfig(*cfgPath)
if err != nil {
return err
}
content, err := os.ReadFile(*in)
if err != nil {
return err
}
objectName := *name
if objectName == "" {
objectName = filepath.Base(*in)
}
want := mirror.SHA256Hex(content)
if !*verifyOnly {
results, perr := mirror.Publish(content, objectName, cfg.Mirrors)
for _, r := range results {
if r.Err != nil {
fmt.Printf(" ✗ %-16s %v\n", r.Name, r.Err)
} else {
fmt.Printf(" ✓ %-16s %s sha256=%s\n", r.Name, r.Path, r.SHA256)
}
}
if perr != nil {
return fmt.Errorf("publish had failures: %w", perr)
}
}
if err := mirror.VerifyConsistency(objectName, cfg.Mirrors, want); err != nil {
return err
}
fmt.Printf("consistency OK: %d/%d mirrors serve sha256=%s\n", len(cfg.Mirrors), len(cfg.Mirrors), want)
return nil
}
func cmdFetch(args []string) error {
fs := flag.NewFlagSet("fetch", flag.ContinueOnError)
cfgPath := fs.String("config", "", "mirrors config JSON")
name := fs.String("name", "", "object name to fetch")
keys := fs.String("keys", "", "optional key ring id=base64pub[,...] to verify signature on fetch")
docType := fs.String("type", "endpoints", "payload type when -keys given: endpoints|notices")
out := fs.String("out", "", "write fetched content here (default: stdout)")
if err := fs.Parse(args); err != nil {
return err
}
if *cfgPath == "" || *name == "" {
return errors.New("-config and -name are required")
}
cfg, err := loadConfig(*cfgPath)
if err != nil {
return err
}
var validate func([]byte) error
if *keys != "" {
ring, err := sign.ParseKeyRing([]string{*keys})
if err != nil {
return err
}
validate = func(b []byte) error {
if *docType == "endpoints" {
_, _, e := endpoint.VerifyDocument(b, ring, 0)
return e
}
env, e := sign.Parse(b)
if e != nil {
return e
}
return sign.Verify(env, ring)
}
}
content, winner, err := mirror.Fetch(*name, cfg.Mirrors, validate)
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "fetched from mirror %q\n", winner)
if *out == "" {
_, err = os.Stdout.Write(content)
return err
}
return os.WriteFile(*out, content, 0o644)
}