7d89ec9d91
- 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>
172 lines
4.7 KiB
Go
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)
|
|
}
|