package main import ( "crypto/sha256" "embed" "encoding/hex" "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "path/filepath" "sort" "strings" "sync" "time" ) // ---- Data model ------------------------------------------------------------- // Asset describes a downloadable artifact for a release. // Keep it minimal and verifiable. // All fields are JSON-tagged for a stable API. type Asset struct { URL string `json:"url"` SHA256 string `json:"sha256"` Size int64 `json:"size_bytes,omitempty"` SignatureURL string `json:"signature_url,omitempty"` ContentType string `json:"content_type,omitempty"` } // Release holds the metadata for a specific Branch→Channel→Arch→Bit→OS combo. // Channel is duplicated in the payload for clarity in responses. type Release struct { Version string `json:"version"` // e.g. 12.3.1 Build string `json:"build,omitempty"` // optional build id ReleasedAt time.Time `json:"released_at"` // RFC3339 NotesURL string `json:"notes_url,omitempty"` Assets []Asset `json:"assets"` Meta map[string]string `json:"meta,omitempty"` // optional free-form ChannelHint string `json:"channel,omitempty"` // echoed by server } // Manifest nests by Branch→Channel→Arch→Bit→OS as requested. // The innermost value is the latest Release for that tuple. // Example path: Releases["12.x"]["stable"]["amd64"]["64"]["windows"] type Manifest struct { Vendor string `json:"vendor"` Product string `json:"product"` DefaultBranch string `json:"default_branch,omitempty"` DefaultChannel string `json:"default_channel,omitempty"` UpdatedAt time.Time `json:"updated_at"` Releases map[string]map[string]map[string]map[string]map[string]Release `json:"releases"` } // publishRequest is the payload for POST /v1/publish type publishRequest struct { Branch string `json:"branch"` Channel string `json:"channel"` // stable, beta, rc, nightly Arch string `json:"arch"` Bit string `json:"bit"` // "32" or "64" OS string `json:"os"` Release Release `json:"release"` } // latestResponse is returned by GET /v1/latest // Mirrors the request tuple alongside the release for clarity. type latestResponse struct { Branch string `json:"branch"` Channel string `json:"channel"` Arch string `json:"arch"` Bit string `json:"bit"` OS string `json:"os"` Release Release `json:"release"` } // ---- Store & persistence ---------------------------------------------------- type store struct { mu sync.RWMutex manifest Manifest path string } func newStore(path, vendor, product string) *store { m := Manifest{ Vendor: vendor, Product: product, DefaultBranch: "", DefaultChannel: "stable", UpdatedAt: time.Now().UTC(), Releases: make(map[string]map[string]map[string]map[string]map[string]Release), } return &store{manifest: m, path: path} } // oldManifest is used to migrate v1 manifests (without channels) → channels("stable"). type oldManifest struct { Vendor string `json:"vendor"` Product string `json:"product"` DefaultBranch string `json:"default_branch,omitempty"` UpdatedAt time.Time `json:"updated_at"` Releases map[string]map[string]map[string]map[string]Release `json:"releases"` } func (s *store) loadIfExists() error { b, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { return nil } return err } var m Manifest if err := json.Unmarshal(b, &m); err == nil && m.Releases != nil { // Looks like v2 → accept. s.mu.Lock() s.manifest = m s.mu.Unlock() return nil } // Try v1 → migrate into channel "stable". var ov1 oldManifest if err := json.Unmarshal(b, &ov1); err != nil { return fmt.Errorf("invalid manifest json: %w", err) } mig := Manifest{ Vendor: ov1.Vendor, Product: ov1.Product, DefaultBranch: ov1.DefaultBranch, DefaultChannel: "stable", UpdatedAt: time.Now().UTC(), Releases: make(map[string]map[string]map[string]map[string]map[string]Release), } for br, archs := range ov1.Releases { if _, ok := mig.Releases[br]; !ok { mig.Releases[br] = make(map[string]map[string]map[string]map[string]Release) } ch := mig.Releases[br] if _, ok := ch["stable"]; !ok { ch["stable"] = make(map[string]map[string]map[string]Release) } for arch, bits := range archs { if _, ok := ch["stable"][arch]; !ok { ch["stable"][arch] = make(map[string]map[string]Release) } for bit, osmap := range bits { if _, ok := ch["stable"][arch][bit]; !ok { ch["stable"][arch][bit] = make(map[string]Release) } for osname, rel := range osmap { ch["stable"][arch][bit][osname] = rel } } } } s.mu.Lock() s.manifest = mig s.mu.Unlock() return nil } func (s *store) persistLocked() error { // Caller must hold s.mu (write) s.manifest.UpdatedAt = time.Now().UTC() b, err := json.MarshalIndent(s.manifest, "", " ") if err != nil { return err } // Ensure dir if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { return err } // Write atomically tmp := s.path + ".tmp" if err := os.WriteFile(tmp, b, 0o644); err != nil { return err } return os.Rename(tmp, s.path) } func (s *store) setLatest(pr publishRequest) error { if err := validateTuple(pr.Branch, pr.Channel, pr.Arch, pr.Bit, pr.OS); err != nil { return err } if err := validateRelease(pr.Release); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() // Create levels if missing lvl1, ok := s.manifest.Releases[pr.Branch] if !ok { lvl1 = make(map[string]map[string]map[string]map[string]Release) s.manifest.Releases[pr.Branch] = lvl1 } lvlCh, ok := lvl1[pr.Channel] if !ok { lvlCh = make(map[string]map[string]map[string]Release) lvl1[pr.Channel] = lvlCh } lvl2, ok := lvlCh[pr.Arch] if !ok { lvl2 = make(map[string]map[string]Release) lvlCh[pr.Arch] = lvl2 } lvl3, ok := lvl2[pr.Bit] if !ok { lvl3 = make(map[string]Release) lvl2[pr.Bit] = lvl3 } rel := pr.Release rel.ChannelHint = pr.Channel lvl3[pr.OS] = rel return s.persistLocked() } func (s *store) getLatest(branch, channel, arch, bit, osname string) (Release, bool) { if err := validateTuple(branch, channel, arch, bit, osname); err != nil { return Release{}, false } s.mu.RLock() defer s.mu.RUnlock() lvl1, ok := s.manifest.Releases[branch] if !ok { return Release{}, false } lvlCh, ok := lvl1[channel] if !ok { return Release{}, false } lvl2, ok := lvlCh[arch] if !ok { return Release{}, false } lvl3, ok := lvl2[bit] if !ok { return Release{}, false } rel, ok := lvl3[osname] return rel, ok } func (s *store) branches() []string { s.mu.RLock() defer s.mu.RUnlock() out := make([]string, 0, len(s.manifest.Releases)) for k := range s.manifest.Releases { out = append(out, k) } sort.Strings(out) return out } // ---- Validation ------------------------------------------------------------- var ( allowedOS = map[string]struct{}{"windows": {}, "linux": {}, "macos": {}, "freebsd": {}} allowedArch = map[string]struct{}{"amd64": {}, "386": {}, "arm64": {}, "armv7": {}, "ppc64le": {}} allowedBit = map[string]struct{}{"64": {}, "32": {}} allowedChannels = map[string]struct{}{"stable": {}, "beta": {}, "rc": {}, "nightly": {}} ) func validateTuple(branch, channel, arch, bit, osname string) error { if strings.TrimSpace(branch) == "" { return errors.New("branch required, e.g. '12.x'") } if _, ok := allowedChannels[channel]; !ok { return fmt.Errorf("invalid channel: %s", channel) } if _, ok := allowedArch[arch]; !ok { return fmt.Errorf("invalid arch: %s", arch) } if _, ok := allowedBit[bit]; !ok { return fmt.Errorf("invalid bit: %s (use '32' or '64')", bit) } if _, ok := allowedOS[osname]; !ok { return fmt.Errorf("invalid os: %s", osname) } return nil } func validateRelease(r Release) error { if strings.TrimSpace(r.Version) == "" { return errors.New("release.version required") } if r.ReleasedAt.IsZero() { return errors.New("release.released_at required (RFC3339)") } if len(r.Assets) == 0 { return errors.New("release.assets must not be empty") } for i, a := range r.Assets { if a.URL == "" { return fmt.Errorf("assets[%d].url required", i) } if a.SHA256 == "" { return fmt.Errorf("assets[%d].sha256 required", i) } } return nil } // ---- HTTP helpers ----------------------------------------------------------- func writeJSON(w http.ResponseWriter, r *http.Request, status int, v any) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") b, err := json.Marshal(v) if err != nil { http.Error(w, "json marshal error", http.StatusInternalServerError) return } etag := sha256.Sum256(b) etagStr := "\"" + hex.EncodeToString(etag[:]) + "\"" w.Header().Set("ETag", etagStr) if inm := r.Header.Get("If-None-Match"); inm != "" && strings.Contains(inm, strings.Trim(etagStr, "\"")) { w.WriteHeader(http.StatusNotModified) return } w.WriteHeader(status) _, _ = w.Write(b) } func parseJSON(r *http.Request, dst any) error { defer r.Body.Close() lr := io.LimitReader(r.Body, 1<<20) // 1 MiB payload cap dec := json.NewDecoder(lr) dec.DisallowUnknownFields() return dec.Decode(dst) } func cors(w http.ResponseWriter, r *http.Request) bool { // Permissive CORS for simplicity (can be tightened later) w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Vary", "Origin") if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type") w.WriteHeader(http.StatusNoContent) return true } return false } // ---- Handlers --------------------------------------------------------------- type server struct { st *store apiToken string // optional; if set, required for POST /v1/publish & /v1/config } func (s *server) handleHealth(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } w.WriteHeader(http.StatusNoContent) } func (s *server) handleManifest(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } s.st.mu.RLock() m := s.st.manifest s.st.mu.RUnlock() writeJSON(w, r, http.StatusOK, m) } func (s *server) handleBranches(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } writeJSON(w, r, http.StatusOK, map[string]any{"branches": s.st.branches()}) } func (s *server) handleChannels(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } keys := make([]string, 0, len(allowedChannels)) for k := range allowedChannels { keys = append(keys, k) } sort.Strings(keys) writeJSON(w, r, http.StatusOK, map[string]any{"channels": keys, "default": s.st.manifest.DefaultChannel}) } func (s *server) handleValues(w http.ResponseWriter, r *http.Request) { // returns allowed enums + defaults to drive the UI if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } archs := keysOf(allowedArch) bits := keysOf(allowedBit) oss := keysOf(allowedOS) chs := keysOf(allowedChannels) s.st.mu.RLock() defBr, defCh := s.st.manifest.DefaultBranch, s.st.manifest.DefaultChannel vendor, product := s.st.manifest.Vendor, s.st.manifest.Product s.st.mu.RUnlock() writeJSON(w, r, http.StatusOK, map[string]any{ "arch": archs, "bit": bits, "os": oss, "channels": chs, "defaults": map[string]string{"branch": defBr, "channel": defCh}, "meta": map[string]string{"vendor": vendor, "product": product}, }) } func keysOf(m map[string]struct{}) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out } func (s *server) handleLatest(w http.ResponseWriter, r *http.Request) { if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } q := r.URL.Query() branch := firstNonEmpty(q.Get("branch"), s.st.manifest.DefaultBranch) channel := firstNonEmpty(q.Get("channel"), s.st.manifest.DefaultChannel) arch := q.Get("arch") bit := q.Get("bit") osname := q.Get("os") if branch == "" || channel == "" || arch == "" || bit == "" || osname == "" { http.Error(w, "missing query params: branch, channel, arch, bit, os", http.StatusBadRequest) return } rel, ok := s.st.getLatest(branch, channel, arch, bit, osname) if !ok { http.Error(w, "not found", http.StatusNotFound) return } writeJSON(w, r, http.StatusOK, latestResponse{Branch: branch, Channel: channel, Arch: arch, Bit: bit, OS: osname, Release: rel}) } func (s *server) handleLatestPath(w http.ResponseWriter, r *http.Request) { // /v1/latest/{branch}/{channel}/{arch}/{bit}/{os} if cors(w, r) { return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/latest/"), "/") if len(parts) != 5 { http.Error(w, "expected /v1/latest/{branch}/{channel}/{arch}/{bit}/{os}", http.StatusBadRequest) return } branch, channel, arch, bit, osname := parts[0], parts[1], parts[2], parts[3], parts[4] rel, ok := s.st.getLatest(branch, channel, arch, bit, osname) if !ok { http.Error(w, "not found", http.StatusNotFound) return } writeJSON(w, r, http.StatusOK, latestResponse{Branch: branch, Channel: channel, Arch: arch, Bit: bit, OS: osname, Release: rel}) } func (s *server) handlePublish(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodOptions { cors(w, r) return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Auth (if token configured) if s.apiToken != "" { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") || strings.TrimPrefix(auth, "Bearer ") != s.apiToken { http.Error(w, "unauthorized", http.StatusUnauthorized) return } } var pr publishRequest if err := parseJSON(r, &pr); err != nil { http.Error(w, fmt.Sprintf("invalid json: %v", err), http.StatusBadRequest) return } if err := s.st.setLatest(pr); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } writeJSON(w, r, http.StatusOK, map[string]string{"status": "ok"}) } // handleConfig allows updating vendor/product/defaults (token required if set) func (s *server) handleConfig(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodOptions { cors(w, r) return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if s.apiToken != "" { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") || strings.TrimPrefix(auth, "Bearer ") != s.apiToken { http.Error(w, "unauthorized", http.StatusUnauthorized) return } } var req struct { Vendor string `json:"vendor"` Product string `json:"product"` DefaultBranch string `json:"default_branch"` DefaultChannel string `json:"default_channel"` } if err := parseJSON(r, &req); err != nil { http.Error(w, fmt.Sprintf("invalid json: %v", err), http.StatusBadRequest) return } if req.DefaultChannel != "" { if _, ok := allowedChannels[req.DefaultChannel]; !ok { http.Error(w, "invalid default_channel", http.StatusBadRequest) return } } s.st.mu.Lock() if req.Vendor != "" { s.st.manifest.Vendor = req.Vendor } if req.Product != "" { s.st.manifest.Product = req.Product } if req.DefaultBranch != "" { s.st.manifest.DefaultBranch = req.DefaultBranch } if req.DefaultChannel != "" { s.st.manifest.DefaultChannel = req.DefaultChannel } if err := s.st.persistLocked(); err != nil { s.st.mu.Unlock() http.Error(w, err.Error(), http.StatusInternalServerError) return } s.st.mu.Unlock() writeJSON(w, r, http.StatusOK, map[string]string{"status": "ok"}) } // ---- Admin UI --------------------------------------------------------------- //go:embed admin.html var adminHTML embed.FS func (s *server) handleAdmin(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") b, err := adminHTML.ReadFile("admin.html") if err != nil { http.Error(w, "admin.html not embedded; ensure //go:embed admin.html and file exists: "+err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) _, _ = w.Write(b) } // ---- Main ------------------------------------------------------------------ func main() { addr := envOr("HTTP_PUBLIC", ":8080") manifestPath := envOr("MANIFEST_PATH", "/data/manifest.json") vendor := envOr("APP_VENDOR", "YourVendor") product := envOr("APP_PRODUCT", "YourProduct") token := os.Getenv("API_TOKEN") // optional; if set, required for POST st := newStore(manifestPath, vendor, product) if err := st.loadIfExists(); err != nil { log.Fatalf("load manifest: %v", err) } srv := &server{st: st, apiToken: token} http.HandleFunc("/healthz", srv.handleHealth) http.HandleFunc("/admin", srv.handleAdmin) // Data API http.HandleFunc("/v1/manifest", srv.handleManifest) http.HandleFunc("/v1/values", srv.handleValues) http.HandleFunc("/v1/branches", srv.handleBranches) http.HandleFunc("/v1/channels", srv.handleChannels) http.HandleFunc("/v1/latest", srv.handleLatest) http.HandleFunc("/v1/latest/", srv.handleLatestPath) http.HandleFunc("/v1/publish", srv.handlePublish) http.HandleFunc("/v1/config", srv.handleConfig) log.Printf("agent listening on %s (admin UI at /admin)", addr) log.Fatal(http.ListenAndServe(addr, nil)) } // ---- Utils ----------------------------------------------------------------- func envOr(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func firstNonEmpty(vals ...string) string { for _, v := range vals { if strings.TrimSpace(v) != "" { return v } } return "" }