package http import ( "fmt" stdhttp "net/http" "os" "path/filepath" "strings" "gitea.dcglab.co.uk/steve/restic-manager/internal/api" ) // agent_assets.go serves the agent binary (one per OS/arch) and the // install scripts. The binaries live under /agent-binaries/, // laid down by the release pipeline (or copied by hand for now). // The install scripts live in /install/ alongside the // systemd unit. // // Both endpoints are intentionally unauthenticated: the install // payload is unprivileged on its own — it's the one-time enrollment // token that grants access. Anyone can pull the binary; only // someone with a valid token can use it productively. // // P1-31: signed-binary verification is deferred. Today we serve // whatever the operator dropped on disk. Future work bumps this to // minisign/cosign signed bundles. // installAssetsRoutes adds /agent/binary and /install/* to r. func (s *Server) handleAgentBinary(w stdhttp.ResponseWriter, r *stdhttp.Request) { osTag := r.URL.Query().Get("os") archTag := r.URL.Query().Get("arch") if osTag == "" || archTag == "" { writeJSONError(w, stdhttp.StatusBadRequest, "missing_os_or_arch", "query params os= and arch= are required") return } if !validOS(osTag) || !validArch(archTag) { writeJSONError(w, stdhttp.StatusBadRequest, "unsupported_target", fmt.Sprintf("os=%q arch=%q not in {linux,windows} × {amd64,arm64}", osTag, archTag)) return } ext := "" if osTag == "windows" { ext = ".exe" } name := fmt.Sprintf("restic-manager-agent-%s-%s%s", osTag, archTag, ext) path := filepath.Join(s.deps.Cfg.DataDir, "agent-binaries", name) if _, err := os.Stat(path); err != nil { writeJSONError(w, stdhttp.StatusNotFound, "binary_not_published", fmt.Sprintf("agent binary for %s/%s not published on this server", osTag, archTag)) return } w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`) stdhttp.ServeFile(w, r, path) } func (s *Server) handleInstallAsset(w stdhttp.ResponseWriter, r *stdhttp.Request) { // chi's TrimPrefix-like behavior: r.URL.Path is "/install/". rel := strings.TrimPrefix(r.URL.Path, "/install/") // Reject any path traversal — must be a flat filename. if rel == "" || strings.ContainsAny(rel, "/\\") { writeJSONError(w, stdhttp.StatusBadRequest, "bad_path", "") return } path := filepath.Join(s.deps.Cfg.DataDir, "install", rel) if _, err := os.Stat(path); err != nil { writeJSONError(w, stdhttp.StatusNotFound, "not_found", "") return } stdhttp.ServeFile(w, r, path) } func validOS(s string) bool { switch api.HostOS(s) { case api.OSLinux, api.OSWindows: return true } return false } func validArch(s string) bool { switch api.HostArch(s) { case api.ArchAmd64, api.ArchArm64: return true } return false }