Proxy missing host-agent binaries from GitHub releases (#1254)

This commit is contained in:
rcourtman 2026-03-25 13:11:31 +00:00
parent 1de1392c9b
commit 2ed1c3b839
3 changed files with 146 additions and 0 deletions

View file

@ -7135,6 +7135,14 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
return
}
// Fallback: proxy from GitHub releases for strict platform/arch requests.
// This mirrors unified-agent behavior and covers installs that were updated
// without refreshing the local host-agent binary bundle.
if platformParam != "" && archParam != "" {
r.proxyHostAgentBinaryFromGitHub(w, req, platformParam, archParam, strings.HasSuffix(req.URL.Path, ".sha256"))
return
}
// Build detailed error message with troubleshooting guidance
var errorMsg strings.Builder
errorMsg.WriteString(fmt.Sprintf("Host agent binary not found for %s/%s\n\n", platformParam, archParam))

View file

@ -117,3 +117,74 @@ func TestHandleDownloadHostAgentAllowsHEAD(t *testing.T) {
t.Fatalf("expected empty body for HEAD, got %d bytes", rr.Body.Len())
}
}
func TestHandleDownloadHostAgent_ProxyFromGitHub(t *testing.T) {
binDir := setupTempPulseBin(t)
router := &Router{
projectRoot: t.TempDir(),
installScriptClient: newTestInstallScriptClient(t, "https://github.com/rcourtman/Pulse/releases/latest/download/pulse-host-agent-freebsd-amd64", http.StatusOK, "freebsd-binary", nil),
}
for _, path := range []string{
filepath.Join(binDir, "pulse-host-agent-freebsd-amd64"),
"/opt/pulse/pulse-host-agent-freebsd-amd64",
filepath.Join("/app", "pulse-host-agent-freebsd-amd64"),
} {
if _, err := os.Stat(path); err == nil {
t.Skipf("local host-agent binary exists at %s; skipping proxy fallback test", path)
}
}
req := httptest.NewRequest(http.MethodGet, "/download/pulse-host-agent?platform=freebsd&arch=amd64", nil)
rr := httptest.NewRecorder()
router.handleDownloadHostAgent(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d body=%s", rr.Code, rr.Body.String())
}
if got := rr.Body.String(); got != "freebsd-binary" {
t.Fatalf("unexpected response body: %q", got)
}
if got := rr.Header().Get("X-Served-From"); got != "github-proxy" {
t.Fatalf("unexpected X-Served-From header: %q", got)
}
expectedChecksum := fmt.Sprintf("%x", sha256.Sum256([]byte("freebsd-binary")))
if got := rr.Header().Get("X-Checksum-Sha256"); got != expectedChecksum {
t.Fatalf("unexpected checksum header: got %q want %q", got, expectedChecksum)
}
}
func TestHandleDownloadHostAgentChecksum_ProxyFromGitHub(t *testing.T) {
binDir := setupTempPulseBin(t)
router := &Router{
projectRoot: t.TempDir(),
installScriptClient: newTestInstallScriptClient(t, "https://github.com/rcourtman/Pulse/releases/latest/download/pulse-host-agent-freebsd-amd64", http.StatusOK, "freebsd-binary", nil),
}
for _, path := range []string{
filepath.Join(binDir, "pulse-host-agent-freebsd-amd64"),
"/opt/pulse/pulse-host-agent-freebsd-amd64",
filepath.Join("/app", "pulse-host-agent-freebsd-amd64"),
} {
if _, err := os.Stat(path); err == nil {
t.Skipf("local host-agent binary exists at %s; skipping proxy checksum fallback test", path)
}
}
req := httptest.NewRequest(http.MethodGet, "/download/pulse-host-agent.sha256?platform=freebsd&arch=amd64", nil)
rr := httptest.NewRecorder()
router.handleDownloadHostAgent(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d body=%s", rr.Code, rr.Body.String())
}
expected := fmt.Sprintf("%x", sha256.Sum256([]byte("freebsd-binary")))
if got := strings.TrimSpace(rr.Body.String()); got != expected {
t.Fatalf("unexpected checksum body: got %q want %q", got, expected)
}
if got := rr.Header().Get("X-Served-From"); got != "github-proxy" {
t.Fatalf("unexpected X-Served-From header: %q", got)
}
}

View file

@ -249,6 +249,73 @@ func (r *Router) proxyAgentBinaryFromGitHub(w http.ResponseWriter, req *http.Req
w.Write(content)
}
// proxyHostAgentBinaryFromGitHub downloads a host-agent binary from GitHub releases and
// serves either the binary (with checksum header) or just the checksum body for .sha256 requests.
func (r *Router) proxyHostAgentBinaryFromGitHub(w http.ResponseWriter, req *http.Request, platform, arch string, checksumOnly bool) {
binaryName := "pulse-host-agent-" + platform + "-" + arch
if platform == "windows" {
binaryName += ".exe"
}
githubURL := "https://github.com/rcourtman/Pulse/releases/latest/download/" + binaryName
log.Info().
Str("platform", platform).
Str("arch", arch).
Str("url", githubURL).
Msg("Local host agent binary not found, proxying from GitHub releases")
client := r.installScriptClient
if client == nil {
client = &http.Client{
Timeout: 5 * time.Minute,
}
}
resp, err := client.Get(githubURL)
if err != nil {
log.Error().Err(err).Str("url", githubURL).Msg("Failed to fetch host agent binary from GitHub")
http.Error(w, "Failed to fetch host agent binary", http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Error().Int("status", resp.StatusCode).Str("url", githubURL).Msg("GitHub returned non-200 status for host agent binary")
http.Error(w, "Host agent binary not found on GitHub", http.StatusNotFound)
return
}
const maxHostAgentBinarySize = 100 * 1024 * 1024
limitedReader := io.LimitReader(resp.Body, maxHostAgentBinarySize+1)
hasher := sha256.New()
content, err := io.ReadAll(io.TeeReader(limitedReader, hasher))
if err != nil {
log.Error().Err(err).Msg("Failed to read host agent binary from GitHub")
http.Error(w, "Failed to read host agent binary", http.StatusInternalServerError)
return
}
if int64(len(content)) > maxHostAgentBinarySize {
log.Error().Int64("size", int64(len(content))).Msg("Host agent binary from GitHub exceeds size limit")
http.Error(w, "Host agent binary too large", http.StatusInternalServerError)
return
}
checksum := hex.EncodeToString(hasher.Sum(nil))
if checksumOnly {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Served-From", "github-proxy")
_, _ = w.Write([]byte(checksum + "\n"))
return
}
w.Header().Set("X-Checksum-Sha256", checksum)
w.Header().Set("X-Served-From", "github-proxy")
w.Header().Set("Content-Type", "application/octet-stream")
_, _ = w.Write(content)
}
// proxyInstallScriptFromGitHub fetches an install script from GitHub releases
// This is used as a fallback when scripts aren't available locally (e.g., LXC updates)
func (r *Router) proxyInstallScriptFromGitHub(w http.ResponseWriter, req *http.Request, scriptName string) {