mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 10:52:08 +00:00
22d6c1d8a5
When the unified agent binary isn't found locally (happens on LXC/barebone
installations that update via web UI which only updates the pulse binary),
redirect to GitHub releases using HTTP 307.
This complements the install.sh GitHub proxy fallback from 7b6613bb.
Related to #909
228 lines
7.8 KiB
Go
228 lines
7.8 KiB
Go
package api
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// handleDownloadUnifiedInstallScript serves the universal install.sh script
|
|
func (r *Router) handleDownloadUnifiedInstallScript(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Prevent caching - always serve the latest version
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
w.Header().Set("Expires", "0")
|
|
|
|
scriptPath := "/opt/pulse/scripts/install.sh"
|
|
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
|
// Fallback to project root (dev environment)
|
|
scriptPath = filepath.Join(r.projectRoot, "scripts", "install.sh")
|
|
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
|
// Final fallback: proxy from GitHub releases
|
|
// This handles LXC/barebone installations updated via web UI where
|
|
// only the binary is updated, not the scripts directory
|
|
log.Info().Msg("Local install.sh not found, proxying from GitHub releases")
|
|
r.proxyInstallScriptFromGitHub(w, req, "install.sh")
|
|
return
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/x-shellscript")
|
|
w.Header().Set("Content-Disposition", "inline; filename=\"install.sh\"")
|
|
http.ServeFile(w, req, scriptPath)
|
|
}
|
|
|
|
// handleDownloadUnifiedInstallScriptPS serves the universal install.ps1 script
|
|
func (r *Router) handleDownloadUnifiedInstallScriptPS(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Prevent caching - always serve the latest version
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
w.Header().Set("Expires", "0")
|
|
|
|
scriptPath := "/opt/pulse/scripts/install.ps1"
|
|
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
|
// Fallback to project root (dev environment)
|
|
scriptPath = filepath.Join(r.projectRoot, "scripts", "install.ps1")
|
|
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
|
// Final fallback: proxy from GitHub releases
|
|
log.Info().Msg("Local install.ps1 not found, proxying from GitHub releases")
|
|
r.proxyInstallScriptFromGitHub(w, req, "install.ps1")
|
|
return
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.Header().Set("Content-Disposition", "inline; filename=\"install.ps1\"")
|
|
http.ServeFile(w, req, scriptPath)
|
|
}
|
|
|
|
// normalizeUnifiedAgentArch normalizes architecture strings for the unified agent.
|
|
func normalizeUnifiedAgentArch(arch string) string {
|
|
arch = strings.ToLower(strings.TrimSpace(arch))
|
|
switch arch {
|
|
case "linux-amd64", "amd64", "x86_64":
|
|
return "linux-amd64"
|
|
case "linux-arm64", "arm64", "aarch64":
|
|
return "linux-arm64"
|
|
case "linux-armv7", "armv7", "armv7l", "armhf":
|
|
return "linux-armv7"
|
|
case "linux-armv6", "armv6":
|
|
return "linux-armv6"
|
|
case "linux-386", "386", "i386", "i686":
|
|
return "linux-386"
|
|
case "darwin-amd64", "macos-amd64":
|
|
return "darwin-amd64"
|
|
case "darwin-arm64", "macos-arm64":
|
|
return "darwin-arm64"
|
|
case "windows-amd64":
|
|
return "windows-amd64"
|
|
case "windows-arm64":
|
|
return "windows-arm64"
|
|
case "windows-386":
|
|
return "windows-386"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// handleDownloadUnifiedAgent serves the pulse-agent binary
|
|
func (r *Router) handleDownloadUnifiedAgent(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Prevent caching - always serve the latest version
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
w.Header().Set("Expires", "0")
|
|
|
|
archParam := strings.TrimSpace(req.URL.Query().Get("arch"))
|
|
searchPaths := make([]string, 0, 6)
|
|
|
|
// If a specific architecture is requested, only look for that architecture
|
|
// Do NOT fall back to generic binary - that could serve the wrong architecture
|
|
normalized := normalizeUnifiedAgentArch(archParam)
|
|
if normalized != "" {
|
|
searchPaths = append(searchPaths,
|
|
filepath.Join(pulseBinDir(), "pulse-agent-"+normalized),
|
|
filepath.Join("/opt/pulse", "pulse-agent-"+normalized),
|
|
filepath.Join("/app", "pulse-agent-"+normalized),
|
|
filepath.Join(r.projectRoot, "bin", "pulse-agent-"+normalized),
|
|
)
|
|
} else {
|
|
// No specific architecture requested - allow fallback to generic binary
|
|
searchPaths = append(searchPaths,
|
|
filepath.Join(pulseBinDir(), "pulse-agent"),
|
|
"/opt/pulse/pulse-agent",
|
|
filepath.Join("/app", "pulse-agent"),
|
|
filepath.Join(r.projectRoot, "bin", "pulse-agent"),
|
|
)
|
|
}
|
|
|
|
for _, candidate := range searchPaths {
|
|
if candidate == "" {
|
|
continue
|
|
}
|
|
|
|
info, err := os.Stat(candidate)
|
|
if err != nil || info.IsDir() {
|
|
continue
|
|
}
|
|
|
|
checksum, err := r.cachedSHA256(candidate, info)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("path", candidate).Msg("Failed to compute unified agent checksum")
|
|
continue
|
|
}
|
|
|
|
file, err := os.Open(candidate)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("path", candidate).Msg("Failed to open unified agent binary for download")
|
|
continue
|
|
}
|
|
|
|
w.Header().Set("X-Checksum-Sha256", checksum)
|
|
http.ServeContent(w, req, filepath.Base(candidate), info.ModTime(), file)
|
|
file.Close()
|
|
return
|
|
}
|
|
|
|
// Fallback: redirect to GitHub releases for the binary
|
|
// This handles LXC/barebone installations that don't have agent binaries locally
|
|
if normalized != "" {
|
|
// Map architecture to GitHub release asset name
|
|
binaryName := "pulse-agent-" + normalized
|
|
if strings.HasPrefix(normalized, "windows-") {
|
|
binaryName += ".exe"
|
|
}
|
|
githubURL := "https://github.com/rcourtman/Pulse/releases/latest/download/" + binaryName
|
|
log.Info().Str("arch", normalized).Str("redirect", githubURL).Msg("Local agent binary not found, redirecting to GitHub releases")
|
|
w.Header().Set("X-Served-From", "github-redirect")
|
|
http.Redirect(w, req, githubURL, http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
|
|
// No architecture specified and no local binary - can't redirect without knowing arch
|
|
http.Error(w, "Agent binary not found. Specify ?arch=linux-amd64 (or your architecture)", http.StatusNotFound)
|
|
}
|
|
|
|
// 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) {
|
|
// Use raw.githubusercontent.com to fetch from main branch
|
|
githubURL := "https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/" + scriptName
|
|
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
resp, err := client.Get(githubURL)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("url", githubURL).Msg("Failed to fetch install script from GitHub")
|
|
http.Error(w, "Failed to fetch install script", 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 install script")
|
|
http.Error(w, "Install script not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Read the script content
|
|
content, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to read install script from GitHub")
|
|
http.Error(w, "Failed to read install script", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Determine content type based on script extension
|
|
contentType := "text/x-shellscript"
|
|
if strings.HasSuffix(scriptName, ".ps1") {
|
|
contentType = "text/plain"
|
|
}
|
|
|
|
w.Header().Set("Content-Type", contentType)
|
|
w.Header().Set("Content-Disposition", "inline; filename=\""+scriptName+"\"")
|
|
w.Header().Set("X-Served-From", "github-fallback")
|
|
w.Write(content)
|
|
}
|