Pulse/internal/relay/proxy_additional_test.go
rcourtman 24b2e40e92 Harden scanned request and storage boundaries
Harden CodeQL-scanned request, command, path, and frontend sinks across relay proxying, availability probes, connection probing, notification CLI execution, report storage, licensing persistence, preview bootstrapping, tooltip rendering, logging, and test identity generation.
2026-07-09 17:22:29 +01:00

175 lines
5.7 KiB
Go

package relay
import (
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/rs/zerolog"
)
func decodeProxyResponse(t *testing.T, data []byte) ProxyResponse {
t.Helper()
var resp ProxyResponse
if err := json.Unmarshal(data, &resp); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
return resp
}
func decodeProxyErrorMessage(t *testing.T, resp ProxyResponse) string {
t.Helper()
bodyBytes, err := base64.StdEncoding.DecodeString(resp.Body)
if err != nil {
t.Fatalf("decode body: %v", err)
}
var payload map[string]string
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
t.Fatalf("unmarshal error payload: %v", err)
}
return payload["error"]
}
func TestNewHTTPProxy_ConfiguresClients(t *testing.T) {
proxy := NewHTTPProxy("127.0.0.1:7655", zerolog.Nop())
if proxy.localAddr != "127.0.0.1:7655" {
t.Errorf("localAddr: got %q, want %q", proxy.localAddr, "127.0.0.1:7655")
}
if proxy.client == nil {
t.Fatal("client is nil")
}
if proxy.streamClient == nil {
t.Fatal("streamClient is nil")
}
if proxy.client.Timeout != proxyRequestTimeout {
t.Errorf("client timeout: got %v, want %v", proxy.client.Timeout, proxyRequestTimeout)
}
req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
if err != nil {
t.Fatalf("new request: %v", err)
}
if err := proxy.client.CheckRedirect(req, nil); err != http.ErrUseLastResponse {
t.Errorf("client redirect policy: got %v, want %v", err, http.ErrUseLastResponse)
}
if err := proxy.streamClient.CheckRedirect(req, nil); err != http.ErrUseLastResponse {
t.Errorf("stream client redirect policy: got %v, want %v", err, http.ErrUseLastResponse)
}
}
func TestHTTPProxy_HandleRequest_ErrorBranches(t *testing.T) {
logger := zerolog.Nop()
t.Run("rejects invalid base64 body", func(t *testing.T) {
proxy := NewHTTPProxy("127.0.0.1:1", logger)
req := ProxyRequest{ID: "req_bad_b64", Method: http.MethodPost, Path: "/api/echo", Body: "%%%not-base64%%%"}
payload, _ := json.Marshal(req)
resp := decodeProxyResponse(t, mustHandleRequest(t, proxy, payload))
if resp.Status != http.StatusBadRequest {
t.Errorf("status: got %d, want %d", resp.Status, http.StatusBadRequest)
}
if got := decodeProxyErrorMessage(t, resp); got != "invalid base64 body" {
t.Errorf("error message: got %q, want %q", got, "invalid base64 body")
}
})
t.Run("rejects oversized request body", func(t *testing.T) {
proxy := NewHTTPProxy("127.0.0.1:1", logger)
req := ProxyRequest{
ID: "req_oversize",
Method: http.MethodPost,
Path: "/api/echo",
Body: base64.StdEncoding.EncodeToString(make([]byte, maxProxyBodySize+1)),
}
payload, _ := json.Marshal(req)
resp := decodeProxyResponse(t, mustHandleRequest(t, proxy, payload))
if resp.Status != http.StatusRequestEntityTooLarge {
t.Errorf("status: got %d, want %d", resp.Status, http.StatusRequestEntityTooLarge)
}
if got := decodeProxyErrorMessage(t, resp); !strings.Contains(got, "request body exceeds") {
t.Errorf("error message: got %q, expected request-body size error", got)
}
})
t.Run("rejects oversized response body", func(t *testing.T) {
mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write(make([]byte, maxProxyBodySize+1))
}))
defer mockAPI.Close()
proxy := NewHTTPProxy(strings.TrimPrefix(mockAPI.URL, "http://"), logger)
req := ProxyRequest{ID: "resp_oversize", Method: http.MethodGet, Path: "/api/large"}
payload, _ := json.Marshal(req)
resp := decodeProxyResponse(t, mustHandleRequest(t, proxy, payload))
if resp.Status != http.StatusRequestEntityTooLarge {
t.Errorf("status: got %d, want %d", resp.Status, http.StatusRequestEntityTooLarge)
}
if got := decodeProxyErrorMessage(t, resp); !strings.Contains(got, "response body exceeds") {
t.Errorf("error message: got %q, expected response-body size error", got)
}
})
t.Run("normalizes paths without leading slash", func(t *testing.T) {
mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, r.URL.Path)
}))
defer mockAPI.Close()
proxy := NewHTTPProxy(strings.TrimPrefix(mockAPI.URL, "http://"), logger)
req := ProxyRequest{ID: "req_path", Method: http.MethodGet, Path: "api/normalized"}
payload, _ := json.Marshal(req)
resp := decodeProxyResponse(t, mustHandleRequest(t, proxy, payload))
if resp.Status != http.StatusOK {
t.Fatalf("status: got %d, want %d", resp.Status, http.StatusOK)
}
body, err := base64.StdEncoding.DecodeString(resp.Body)
if err != nil {
t.Fatalf("decode body: %v", err)
}
if got, want := string(body), "/api/normalized"; got != want {
t.Errorf("proxied path: got %q, want %q", got, want)
}
})
}
func TestNormalizeProxyRequestURIRejectsNonOriginRelativePaths(t *testing.T) {
for _, rawPath := range []string{
"http://169.254.169.254/latest/meta-data",
"//169.254.169.254/latest/meta-data",
} {
t.Run(rawPath, func(t *testing.T) {
if _, err := normalizeProxyRequestURI(rawPath); err == nil {
t.Fatalf("expected %q to be rejected", rawPath)
}
})
}
}
func TestNormalizeProxyRequestURIPreservesLocalPathAndQuery(t *testing.T) {
got, err := normalizeProxyRequestURI("api/resources?filter=http%3A%2F%2Fexample.test")
if err != nil {
t.Fatalf("normalizeProxyRequestURI() error = %v", err)
}
if got.RequestURI() != "/api/resources?filter=http%3A%2F%2Fexample.test" {
t.Fatalf("RequestURI() = %q", got.RequestURI())
}
}
func mustHandleRequest(t *testing.T, proxy *HTTPProxy, payload []byte) []byte {
t.Helper()
resp, err := proxy.HandleRequest(payload, "token")
if err != nil {
t.Fatalf("HandleRequest() error = %v", err)
}
return resp
}