mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
78023e0e42
Change-source: pulse-maintainer
680 lines
27 KiB
Go
680 lines
27 KiB
Go
package api
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupUnifiedAgentRouter(t *testing.T) (*Router, string) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Create required directories
|
|
err := os.MkdirAll(filepath.Join(tempDir, "scripts"), 0755)
|
|
require.NoError(t, err)
|
|
err = os.MkdirAll(filepath.Join(tempDir, "bin"), 0755)
|
|
require.NoError(t, err)
|
|
|
|
router := &Router{
|
|
projectRoot: tempDir,
|
|
checksumCache: make(map[string]checksumCacheEntry),
|
|
}
|
|
|
|
return router, tempDir
|
|
}
|
|
|
|
// expectedAgentVersionForTest mirrors what the download handler will look for,
|
|
// so fixtures that stand in for a healthy agent carry the version a real build
|
|
// would. Without it every "valid" fixture reads as a stale build.
|
|
func expectedAgentVersionForTest() string {
|
|
versionInfo, err := updates.GetCurrentVersion()
|
|
if err != nil || versionInfo == nil {
|
|
return ""
|
|
}
|
|
return expectedAgentBinaryVersion(versionInfo.Version)
|
|
}
|
|
|
|
func validTestUnifiedAgentBinary(suffix string) []byte {
|
|
return []byte("ELF test binary " + canonicalUnifiedAgentReportPath + " " +
|
|
expectedAgentVersionForTest() + " " + suffix)
|
|
}
|
|
|
|
func staleTestUnifiedAgentBinary(suffix string) []byte {
|
|
return []byte("ELF stale binary " + legacyUnifiedAgentReportPath + " " + suffix)
|
|
}
|
|
|
|
func encodedTestSSHSignature(payload string) string {
|
|
return encodeSSHSignatureForHeader([]byte(payload))
|
|
}
|
|
|
|
func TestDownloadInstallScript_Local(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
|
|
// Create dummy script
|
|
scriptContent := "#!/bin/bash\necho 'installing'"
|
|
scriptPath := filepath.Join(tempDir, "scripts", "install.sh")
|
|
err := os.WriteFile(scriptPath, []byte(scriptContent), 0644)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/install.sh", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedInstallScript(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, scriptContent, w.Body.String())
|
|
assert.Equal(t, "text/x-shellscript", w.Header().Get("Content-Type"))
|
|
}
|
|
|
|
// TestDownloadInstallScript_PublishedReleaseUnsignedLocalServesAgentInstaller
|
|
// pins the issue #1470 closure: when the local agent installer is present on a
|
|
// published-release server but its .sig/.sshsig sidecars are not, the endpoint
|
|
// must serve the LOCAL agent installer, never proxy the GitHub install.sh asset
|
|
// (which is the SERVER installer). This is the state every pre-fix LXC/systemd
|
|
// install is in until it redeploys the sidecars.
|
|
func TestDownloadInstallScript_PublishedReleaseUnsignedLocalServesAgentInstaller(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.6"
|
|
// A client that fails any outbound call, so a regression that re-introduces
|
|
// the GitHub proxy fallback surfaces as a test failure rather than silently
|
|
// serving the server installer.
|
|
router.installScriptClient = newTestInstallScriptClient(t, http.MethodGet, "", 0, "", errors.New("proxy must not be used"))
|
|
|
|
scriptContent := "#!/usr/bin/env bash\n# Pulse Unified Agent Installer\necho 'agent'"
|
|
scriptPath := filepath.Join(tempDir, "scripts", "install.sh")
|
|
require.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0644))
|
|
// Note: no .sig / .sshsig sidecars written.
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/install.sh", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedInstallScript(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, scriptContent, w.Body.String())
|
|
assert.NotEqual(t, "github-fallback", w.Header().Get("X-Served-From"))
|
|
}
|
|
|
|
func TestDownloadInstallScriptPS_Local(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
|
|
// Create dummy script
|
|
scriptContent := "Write-Host 'installing'"
|
|
scriptPath := filepath.Join(tempDir, "scripts", "install.ps1")
|
|
err := os.WriteFile(scriptPath, []byte(scriptContent), 0644)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/install.ps1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedInstallScriptPS(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, scriptContent, w.Body.String())
|
|
assert.Equal(t, "text/plain", w.Header().Get("Content-Type"))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_Local_Generic(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
|
|
// Create dummy binary in project root / bin
|
|
binContent := validTestUnifiedAgentBinary("generic")
|
|
binPath := filepath.Join(tempDir, "bin", "pulse-agent")
|
|
err := os.WriteFile(binPath, binContent, 0755)
|
|
require.NoError(t, err)
|
|
|
|
// Since cachedSHA256 might not be initialized or working without real file usage pattern,
|
|
// checking if our manual Router setup handles it.
|
|
// cachedSHA256 needs 'checksumCache' map initialized which we did in setupUnifiedAgentRouter.
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
// Handle calls r.cachedSHA256 which reads the file
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
// We expect success if cachedSHA256 works
|
|
if w.Code == http.StatusInternalServerError {
|
|
// If cachedSHA256 fails (maybe because it's not exported or implemented elsewhere
|
|
// and depends on something I missed), we will fail here.
|
|
// cachedSHA256 is called in unified_agent.go but defined presumably in router.go or router_utils.go (unexported).
|
|
// I initialized checksumCache so it should work.
|
|
t.Logf("Handler returned 500: %s", w.Body.String())
|
|
}
|
|
|
|
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
|
assert.Equal(t, string(binContent), w.Body.String())
|
|
|
|
// Verify Checksum Header
|
|
hash := sha256.Sum256(binContent)
|
|
expectedChecksum := hex.EncodeToString(hash[:])
|
|
assert.Equal(t, expectedChecksum, w.Header().Get("X-Checksum-Sha256"))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_Local_SpecificArch(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
|
|
// Create dummy binary for linux-amd64
|
|
binContent := validTestUnifiedAgentBinary("linux-amd64")
|
|
binPath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64")
|
|
err := os.WriteFile(binPath, binContent, 0755)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, string(binContent), w.Body.String())
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_LocalReleaseBinaryIncludesSignatureHeader(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0"
|
|
|
|
binContent := validTestUnifiedAgentBinary("linux-amd64")
|
|
binPath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64")
|
|
err := os.WriteFile(binPath, binContent, 0755)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(binPath+".sig", []byte("signed-local-agent"), 0644)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(binPath+".sshsig", []byte("signed-local-agent-ssh"), 0644)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "signed-local-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-local-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
// Regression coverage for #1820: release images carry Windows binaries and
|
|
// detached signatures under the canonical .exe asset name. Looking up an
|
|
// extensionless compatibility symlink made signature verification probe the
|
|
// wrong sidecar names, reject the healthy local binary, and fall through to a
|
|
// GitHub asset that might not exist yet.
|
|
func TestDownloadUnifiedAgent_LocalWindowsReleaseUsesExecutableSignatures(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.4.2"
|
|
router.installScriptClient = newTestInstallScriptClient(t, http.MethodGet, "", 0, "", errors.New("proxy must not be used"))
|
|
|
|
binContent := validTestUnifiedAgentBinary("windows-amd64")
|
|
binPath := filepath.Join(tempDir, "bin", "pulse-agent-windows-amd64.exe")
|
|
require.NoError(t, os.WriteFile(binPath, binContent, 0755))
|
|
require.NoError(t, os.WriteFile(binPath+".sig", []byte("signed-windows-agent"), 0644))
|
|
require.NoError(t, os.WriteFile(binPath+".sshsig", []byte("signed-windows-agent-ssh"), 0644))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=windows-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
|
assert.Equal(t, string(binContent), w.Body.String())
|
|
assert.Equal(t, "signed-windows-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-windows-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
assert.NotEqual(t, "github-proxy", w.Header().Get("X-Served-From"))
|
|
}
|
|
|
|
func TestUnifiedAgentLocalBuildCommandUsesWindowsExecutableName(t *testing.T) {
|
|
assert.Contains(t, unifiedAgentLocalBuildCommand("windows-amd64"), "-o bin/pulse-agent-windows-amd64.exe")
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_SkipsStaleLocalBinaryAndProxies(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
stalePath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64")
|
|
require.NoError(t, os.WriteFile(stalePath, staleTestUnifiedAgentBinary("linux-amd64"), 0755))
|
|
|
|
binaryContent := "fresh github binary"
|
|
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64"
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, binaryContent, w.Body.String())
|
|
assert.Equal(t, "github-proxy", w.Header().Get("X-Served-From"))
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_DevModeRejectsStaleLocalBinary(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "dev"
|
|
|
|
stalePath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64")
|
|
require.NoError(t, os.WriteFile(stalePath, staleTestUnifiedAgentBinary("linux-amd64"), 0755))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
assert.Contains(t, w.Body.String(), "stale or incompatible")
|
|
assert.Contains(t, w.Body.String(), legacyUnifiedAgentReportPath)
|
|
assert.Contains(t, w.Body.String(), "go build -o bin/pulse-agent-linux-amd64")
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
// Ensure NO local files exist (temp dir is empty of binaries)
|
|
// Set up a mock HTTP client to simulate GitHub response
|
|
binaryContent := "fake binary content for proxy test"
|
|
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64"
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
// Should proxy the binary with checksum header instead of redirecting
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, binaryContent, w.Body.String())
|
|
assert.Equal(t, "github-proxy", w.Header().Get("X-Served-From"))
|
|
|
|
// Verify checksum header is present and correct
|
|
hash := sha256.Sum256([]byte(binaryContent))
|
|
expectedChecksum := hex.EncodeToString(hash[:])
|
|
assert.Equal(t, expectedChecksum, w.Header().Get("X-Checksum-Sha256"))
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_UsesConfiguredRepo(t *testing.T) {
|
|
t.Setenv("PULSE_GITHUB_REPO", "example/pulse-fork")
|
|
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
binaryContent := "fake binary content for proxy test"
|
|
expectedURL := "https://github.com/example/pulse-fork/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64"
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, binaryContent, w.Body.String())
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_Windows(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
binaryContent := "MZ fake windows binary"
|
|
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-windows-amd64.exe"
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=windows-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, binaryContent, w.Body.String())
|
|
assert.NotEmpty(t, w.Header().Get("X-Checksum-Sha256"))
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_ArchiveFallback_Darwin(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v9.9.9"
|
|
|
|
binaryContent := []byte("darwin arm64 binary payload")
|
|
archivePayload := buildTestTarGz(t, "pulse-agent-darwin-arm64", binaryContent)
|
|
binaryURL := "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-agent-darwin-arm64"
|
|
signatureURL := binaryURL + ".sig"
|
|
sshSignatureURL := binaryURL + ".sshsig"
|
|
archiveURL := "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-agent-v9.9.9-darwin-arm64.tar.gz"
|
|
|
|
router.installScriptClient = &http.Client{
|
|
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
switch req.URL.String() {
|
|
case binaryURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusNotFound,
|
|
Status: "404 Not Found",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("not found")),
|
|
}, nil
|
|
case archiveURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(bytes.NewReader(archivePayload)),
|
|
}, nil
|
|
case signatureURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("signed-agent")),
|
|
}, nil
|
|
case sshSignatureURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("signed-agent-ssh")),
|
|
}, nil
|
|
default:
|
|
t.Fatalf("unexpected URL: %s", req.URL.String())
|
|
return nil, nil
|
|
}
|
|
}),
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=darwin-arm64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "github-proxy-archive", w.Header().Get("X-Served-From"))
|
|
assert.Equal(t, string(binaryContent), w.Body.String())
|
|
|
|
hash := sha256.Sum256(binaryContent)
|
|
expectedChecksum := hex.EncodeToString(hash[:])
|
|
assert.Equal(t, expectedChecksum, w.Header().Get("X-Checksum-Sha256"))
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_ArchiveFallback_UsesConfiguredRepo(t *testing.T) {
|
|
t.Setenv("PULSE_GITHUB_REPO", "example/pulse-fork")
|
|
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v9.9.9"
|
|
|
|
binaryContent := []byte("darwin arm64 binary payload")
|
|
archivePayload := buildTestTarGz(t, "pulse-agent-darwin-arm64", binaryContent)
|
|
binaryURL := "https://github.com/example/pulse-fork/releases/download/v9.9.9/pulse-agent-darwin-arm64"
|
|
signatureURL := binaryURL + ".sig"
|
|
sshSignatureURL := binaryURL + ".sshsig"
|
|
archiveURL := "https://github.com/example/pulse-fork/releases/download/v9.9.9/pulse-agent-v9.9.9-darwin-arm64.tar.gz"
|
|
|
|
router.installScriptClient = &http.Client{
|
|
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
switch req.URL.String() {
|
|
case binaryURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusNotFound,
|
|
Status: "404 Not Found",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("not found")),
|
|
}, nil
|
|
case archiveURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(bytes.NewReader(archivePayload)),
|
|
}, nil
|
|
case signatureURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("signed-agent")),
|
|
}, nil
|
|
case sshSignatureURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("signed-agent-ssh")),
|
|
}, nil
|
|
default:
|
|
t.Fatalf("unexpected URL: %s", req.URL.String())
|
|
return nil, nil
|
|
}
|
|
}),
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=darwin-arm64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "github-proxy-archive", w.Header().Get("X-Served-From"))
|
|
assert.Equal(t, string(binaryContent), w.Body.String())
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_NotFound(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64", Status: http.StatusNotFound, Body: ""},
|
|
{Method: http.MethodGet, URL: "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-v6.0.0-rc.1-linux-amd64.tar.gz", Status: http.StatusNotFound, Body: ""},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_Error(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
// GitHub is unreachable
|
|
router.installScriptClient = newTestInstallScriptClient(t, http.MethodGet, "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64", 0, "", errors.New("connection refused"))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_DevPrereleaseRejectsGitHubFallback(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-dev"
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
assert.Contains(t, w.Body.String(), "dev mode")
|
|
assert.Contains(t, w.Body.String(), "go build -o bin/pulse-agent-linux-amd64")
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_DevModeReportsRequestedLocalBuildCommand(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "dev"
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=darwin-arm64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Agent binary not found for \"darwin-arm64\" in dev mode.")
|
|
assert.Contains(t, w.Body.String(), "CGO_ENABLED=0 GOOS=darwin GOARCH=arm64")
|
|
assert.Contains(t, w.Body.String(), "go build -o bin/pulse-agent-darwin-arm64 ./cmd/pulse-agent")
|
|
assert.NotContains(t, w.Body.String(), "bin/pulse-agent-linux-amd64")
|
|
}
|
|
|
|
func TestUnifiedAgentLocalBuildCommandHandlesArmVariants(t *testing.T) {
|
|
assert.Equal(
|
|
t,
|
|
"CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o bin/pulse-agent-linux-armv7 ./cmd/pulse-agent",
|
|
unifiedAgentLocalBuildCommand("linux-armv7"),
|
|
)
|
|
assert.Equal(
|
|
t,
|
|
"CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o bin/pulse-agent-linux-armv6 ./cmd/pulse-agent",
|
|
unifiedAgentLocalBuildCommand("linux-armv6"),
|
|
)
|
|
}
|
|
|
|
func TestNormalizeUnifiedAgentArch(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"amd64", "linux-amd64"},
|
|
{"x86_64", "linux-amd64"},
|
|
{"linux-amd64", "linux-amd64"},
|
|
{"arm64", "linux-arm64"},
|
|
{"aarch64", "linux-arm64"},
|
|
{"windows-amd64", "windows-amd64"},
|
|
{"darwin-arm64", "darwin-arm64"},
|
|
{"unknown", ""},
|
|
{"", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, normalizeUnifiedAgentArch(tt.input))
|
|
})
|
|
}
|
|
}
|
|
|
|
func buildTestTarGz(t *testing.T, name string, payload []byte) []byte {
|
|
t.Helper()
|
|
|
|
var buf bytes.Buffer
|
|
gzw := gzip.NewWriter(&buf)
|
|
tw := tar.NewWriter(gzw)
|
|
|
|
header := &tar.Header{
|
|
Name: name,
|
|
Mode: 0o755,
|
|
Size: int64(len(payload)),
|
|
}
|
|
require.NoError(t, tw.WriteHeader(header))
|
|
_, err := tw.Write(payload)
|
|
require.NoError(t, err)
|
|
require.NoError(t, tw.Close())
|
|
require.NoError(t, gzw.Close())
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// writeAgentBinaryFixture builds a stand-in for a compiled agent. The real
|
|
// validator is a byte scan, so a file carrying the same needles exercises it
|
|
// faithfully without shipping a multi-megabyte binary into the test tree.
|
|
func writeAgentBinaryFixture(t *testing.T, reportPath string, versionString string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "pulse-agent")
|
|
body := "\x7fELF padding " + reportPath + " more padding " + versionString + " trailing\n"
|
|
if err := os.WriteFile(path, []byte(body), 0o755); err != nil {
|
|
t.Fatalf("write agent fixture: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestExpectedAgentBinaryVersionDropsBuildMetadata(t *testing.T) {
|
|
cases := map[string]string{
|
|
// A dev server carries git metadata the agent never has.
|
|
"6.2.0-rc.8+git.44.gdd72bd149.dirty": "v6.2.0-rc.8",
|
|
"6.2.0-rc.8": "v6.2.0-rc.8",
|
|
"v6.1.2": "v6.1.2",
|
|
// Unknown versions disable the check rather than rejecting everything.
|
|
// "dev-pro" is the enterprise build's compiled-in placeholder: it is
|
|
// why the caller resolves the version through updates.GetCurrentVersion
|
|
// instead of the compiled-in serverVersion, because a dev server is
|
|
// exactly where agent binaries go stale and this value would silently
|
|
// switch the freshness check off there.
|
|
"dev": "",
|
|
"dev-pro": "",
|
|
"": "",
|
|
"not-a-version": "",
|
|
}
|
|
for input, want := range cases {
|
|
if got := expectedAgentBinaryVersion(input); got != want {
|
|
t.Errorf("expectedAgentBinaryVersion(%q) = %q, want %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateUnifiedAgentBinaryRejectsStaleBuild(t *testing.T) {
|
|
// The shape that broke a live host: a binary on the canonical report
|
|
// contract, so every existing check passes, but built weeks earlier.
|
|
stale := writeAgentBinaryFixture(t, canonicalUnifiedAgentReportPath, "v6.0.5-54-gc862fb0ca0")
|
|
|
|
if err := validateUnifiedAgentBinary(stale, "v6.2.0-rc.8"); err == nil {
|
|
t.Fatal("a binary that does not carry this server's agent version must be refused")
|
|
} else if !strings.Contains(err.Error(), "stale build") {
|
|
t.Fatalf("error should name staleness, got %v", err)
|
|
}
|
|
|
|
// Premise check: without the version guard this same binary is accepted,
|
|
// which is exactly how it reached a host in the first place.
|
|
if err := validateUnifiedAgentBinary(stale, ""); err != nil {
|
|
t.Fatalf("premise check failed: the stale binary is supposed to pass every other check, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateUnifiedAgentBinaryAcceptsMatchingBuild(t *testing.T) {
|
|
current := writeAgentBinaryFixture(t, canonicalUnifiedAgentReportPath, "v6.2.0-rc.8")
|
|
if err := validateUnifiedAgentBinary(current, "v6.2.0-rc.8"); err != nil {
|
|
t.Fatalf("a binary carrying this server's agent version must be served: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateUnifiedAgentBinaryStillRejectsLegacyContract(t *testing.T) {
|
|
// The version guard must not displace the endpoint guard: a correctly
|
|
// versioned binary on the deprecated report path is still refused.
|
|
legacy := writeAgentBinaryFixture(t, legacyUnifiedAgentReportPath, "v6.2.0-rc.8")
|
|
if err := validateUnifiedAgentBinary(legacy, "v6.2.0-rc.8"); err == nil {
|
|
t.Fatal("a binary on the deprecated report endpoint must still be refused")
|
|
}
|
|
}
|