// publish-manifest installs an already signed manifest into a static API root. // Run it behind the separately administered publisher's authentication boundary. package main import ( "bytes" "encoding/json" "errors" "flag" "fmt" "io" "os" "path/filepath" "time" "gitgud.io/mike/mpv-manager/internal/fileops" "gitgud.io/mike/mpv-manager/pkg/releasemanifest" "gitgud.io/mike/mpv-manager/pkg/version" ) func main() { if err := run(os.Args[1:], os.Stdout); err != nil { fmt.Fprintln(os.Stderr, "Manifest publication failed:", err) os.Exit(1) } } func run(args []string, output io.Writer) error { flags := flag.NewFlagSet("publish-manifest", flag.ContinueOnError) flags.SetOutput(output) input := flags.String("manifest", "", "already signed manifest file") root := flags.String("api-dir", "", "static site's API directory") channel := flags.String("channel", "", "authorized destination channel: stable or rc") if err := flags.Parse(args); err != nil { return err } if flags.NArg() != 0 || *input == "" || *root == "" || (*channel != releasemanifest.StableChannel && *channel != releasemanifest.ReleaseCandidateChannel) { return errors.New("require -manifest FILE -api-dir DIRECTORY -channel stable|rc") } keys, err := releasemanifest.ParseTrustedKeys(os.Getenv("MANIFEST_PUBLIC_KEYS")) if err != nil { return err } if len(keys) == 0 { return errors.New("MANIFEST_PUBLIC_KEYS must contain the publisher's independently configured trust ring") } data, err := readManifest(*input) if err != nil { return err } var incoming releasemanifest.Manifest if err := json.Unmarshal(data, &incoming); err != nil { return err } if err := incoming.Verify(keys); err != nil { return err } if err := incoming.Validate(); err != nil { return err } if incoming.Channel != *channel { return fmt.Errorf("signed channel %q does not match authorized destination %q", incoming.Channel, *channel) } published, _ := time.Parse(time.RFC3339, incoming.PublishedAt) if published.After(time.Now().UTC().Add(5 * time.Minute)) { return errors.New("publication time is in the future") } feed := filepath.Join(*root, "releases", *channel+".json") if err := os.MkdirAll(filepath.Dir(feed), 0755); err != nil { return err } err = fileops.WithLock(filepath.Join(*root, "manifest-publication"), func() error { previousData, err := readManifest(feed) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } if err == nil { var previous releasemanifest.Manifest if err := json.Unmarshal(previousData, &previous); err != nil { return err } if err := previous.Verify(keys); err != nil { return fmt.Errorf("existing feed trust: %w", err) } if err := previous.Validate(); err != nil { return err } if previous.Channel != incoming.Channel { return errors.New("existing feed channel mismatch") } previousTime, _ := time.Parse(time.RFC3339, previous.PublishedAt) if version.CompareVersions(incoming.Version, previous.Version) < 0 || published.Before(previousTime) { return errors.New("refusing feed rollback") } if published.Equal(previousTime) { left, _ := previous.CanonicalPayload() right, _ := incoming.CanonicalPayload() if !bytes.Equal(left, right) { return errors.New("refusing feed equivocation") } } } // The RC destination never reads or writes the legacy stable endpoint. legacy := filepath.Join(*root, "releases.json") if incoming.Channel == releasemanifest.StableChannel { legacyData, err := readManifest(legacy) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } if err == nil { var old struct { Version string `json:"version"` } if err := json.Unmarshal(legacyData, &old); err != nil { return err } if old.Version == "" || version.CompareVersions(incoming.Version, old.Version) < 0 { return errors.New("refusing legacy endpoint rollback") } } } if err := fileops.AtomicWrite(feed, data, 0644); err != nil { return err } if incoming.Channel == releasemanifest.StableChannel { // Each document replacement is atomic. On interruption between writes, // legacy clients retain the previous stable release; retry repairs the mirror. if err := fileops.AtomicWrite(legacy, data, 0644); err != nil { return err } } return nil }) if err != nil { return err } fmt.Fprintf(output, "Published %s %s to %s\n", incoming.Channel, incoming.Version, feed) return nil } func readManifest(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() data, err := io.ReadAll(io.LimitReader(f, releasemanifest.MaxManifestBytes+1)) if err != nil { return nil, err } if len(data) > releasemanifest.MaxManifestBytes { return nil, errors.New("manifest exceeds size limit") } return data, nil }