4b70e31cf4
Critical fixes:
- logs --job: validate ^[A-Za-z0-9_-]+$ + shellQuote (was %q backtick RCE)
- pprof: isLoopback treats empty host as bind-all (was :6060 bypass)
- backup restore: filepath.Rel containment check (was tar-slip via a/../..)
- WebAuthn reg auth deferred to P04 (requires session infra)
High fixes:
- txn rollback/show/apply: validate ^T-[0-9a-f]{16}$ + shellQuote
- nft diff --against: validate txn ID before filepath.Join
- drain stopAlloc: validate allocID ^[A-Za-z0-9_-]+$
- cluster_compat: shellQuote peer dir name
- podman image: shellQuote (was %q backtick injection)
- nft TrustedProbes: net.ParseIP/CIDR validation + split v4/v6 sets
- sudoers: validate --proxmox-user/--proxmox-role ^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$
fixed path /etc/sudoers.d/orca; shellQuote pveum/useradd; validateSudoers
checks actual file
- nft country block: validate ^[A-Z]{2}$ (was len==2 only)
New file: internal/cli/validate.go (shared validators + shellQuote)
All 38 Go test packages pass. go vet + gofmt clean.
---ci---
project: orca
phase: 2
milestone: v0.13
status: complete
requirements:
covered: [150]
---/ci---
303 lines
7.6 KiB
Go
303 lines
7.6 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/store"
|
|
)
|
|
|
|
func TestStartPprof_Disabled(t *testing.T) {
|
|
srv, err := StartPprof("", slog.Default())
|
|
if err != nil {
|
|
t.Fatalf("StartPprof(\"\", _) returned err: %v", err)
|
|
}
|
|
if srv != nil {
|
|
t.Fatalf("StartPprof(\"\", _) returned non-nil server: %v", srv)
|
|
}
|
|
}
|
|
|
|
func TestStartPprof_Enabled(t *testing.T) {
|
|
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
addr := ln.Addr().String()
|
|
_ = ln.Close()
|
|
|
|
srv, err := StartPprof(addr, log)
|
|
if err != nil {
|
|
t.Fatalf("StartPprof returned err: %v", err)
|
|
}
|
|
if srv == nil {
|
|
t.Fatal("StartPprof returned nil server for non-empty addr")
|
|
}
|
|
t.Cleanup(func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
_ = srv.Shutdown(ctx)
|
|
})
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
var base string
|
|
for time.Now().Before(deadline) {
|
|
conn, derr := net.DialTimeout("tcp", addr, 50*time.Millisecond)
|
|
if derr == nil {
|
|
_ = conn.Close()
|
|
base = "http://" + addr
|
|
break
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
if base == "" {
|
|
t.Fatal("pprof server did not start listening")
|
|
}
|
|
|
|
client := &http.Client{Timeout: 500 * time.Millisecond}
|
|
for _, path := range []string{"/debug/pprof/", "/debug/pprof/cmdline", "/debug/pprof/heap"} {
|
|
resp, gerr := client.Get(base + path)
|
|
if gerr != nil {
|
|
t.Errorf("GET %s: %v", path, gerr)
|
|
continue
|
|
}
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
t.Errorf("GET %s: expected 200, got %d", path, resp.StatusCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStartPprof_Shutdown(t *testing.T) {
|
|
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
addr := ln.Addr().String()
|
|
_ = ln.Close()
|
|
|
|
srv, err := StartPprof(addr, log)
|
|
if err != nil {
|
|
t.Fatalf("StartPprof returned err: %v", err)
|
|
}
|
|
if srv == nil {
|
|
t.Fatal("StartPprof returned nil server")
|
|
}
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
conn, derr := net.DialTimeout("tcp", addr, 50*time.Millisecond)
|
|
if derr == nil {
|
|
_ = conn.Close()
|
|
break
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
t.Fatalf("Shutdown: %v", err)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 300 * time.Millisecond}
|
|
_, gerr := client.Get("http://" + addr + "/debug/pprof/")
|
|
if gerr == nil {
|
|
t.Error("expected GET to fail after Shutdown, but it succeeded")
|
|
}
|
|
}
|
|
|
|
func TestStartPprof_MuxIsolated(t *testing.T) {
|
|
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
addr := ln.Addr().String()
|
|
_ = ln.Close()
|
|
|
|
srv, err := StartPprof(addr, log)
|
|
if err != nil {
|
|
t.Fatalf("StartPprof returned err: %v", err)
|
|
}
|
|
if srv == nil {
|
|
t.Fatal("StartPprof returned nil server")
|
|
}
|
|
t.Cleanup(func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
_ = srv.Shutdown(ctx)
|
|
})
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
conn, derr := net.DialTimeout("tcp", addr, 50*time.Millisecond)
|
|
if derr == nil {
|
|
_ = conn.Close()
|
|
break
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 500 * time.Millisecond}
|
|
resp, err := client.Get("http://" + addr + "/healthz")
|
|
if err != nil {
|
|
t.Fatalf("GET /healthz: %v", err)
|
|
}
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode != 404 {
|
|
t.Errorf("expected /healthz to 404 on pprof-only mux, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestServer_WithPprof(t *testing.T) {
|
|
db, err := store.Open(filepath.Join(t.TempDir(), "pprof.db"))
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen main: %v", err)
|
|
}
|
|
mainAddr := ln.Addr().String()
|
|
|
|
pln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen pprof: %v", err)
|
|
}
|
|
pprofAddr := pln.Addr().String()
|
|
_ = pln.Close()
|
|
|
|
s := NewServer(Options{
|
|
DB: db,
|
|
Log: log,
|
|
Addr: mainAddr,
|
|
PprofAddr: pprofAddr,
|
|
})
|
|
s.MarkReady()
|
|
|
|
if s.pprofServer == nil {
|
|
t.Fatal("expected pprofServer to be non-nil after NewServer with PprofAddr")
|
|
}
|
|
|
|
errCh := make(chan error, 2)
|
|
go func() {
|
|
err := s.httpServer.Serve(ln)
|
|
if err != nil && err != http.ErrServerClosed {
|
|
errCh <- err
|
|
}
|
|
}()
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
conn, derr := net.DialTimeout("tcp", pprofAddr, 50*time.Millisecond)
|
|
if derr == nil {
|
|
_ = conn.Close()
|
|
break
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 500 * time.Millisecond}
|
|
resp, err := client.Get("http://" + mainAddr + "/healthz")
|
|
if err != nil {
|
|
t.Fatalf("GET main /healthz: %v", err)
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
t.Errorf("main /healthz: expected 200, got %d", resp.StatusCode)
|
|
}
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
_ = resp.Body.Close()
|
|
|
|
presp, err := client.Get("http://" + pprofAddr + "/debug/pprof/")
|
|
if err != nil {
|
|
t.Fatalf("GET pprof /debug/pprof/: %v", err)
|
|
}
|
|
if presp.StatusCode != 200 {
|
|
t.Errorf("pprof /debug/pprof/: expected 200, got %d", presp.StatusCode)
|
|
}
|
|
_, _ = io.Copy(io.Discard, presp.Body)
|
|
_ = presp.Body.Close()
|
|
|
|
presp, err = client.Get("http://" + pprofAddr + "/healthz")
|
|
if err != nil {
|
|
t.Fatalf("GET pprof /healthz: %v", err)
|
|
}
|
|
_, _ = io.Copy(io.Discard, presp.Body)
|
|
_ = presp.Body.Close()
|
|
if presp.StatusCode != 404 {
|
|
t.Errorf("expected /healthz 404 on pprof mux, got %d", presp.StatusCode)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
if err := s.Shutdown(ctx); err != nil {
|
|
t.Errorf("Shutdown: %v", err)
|
|
}
|
|
|
|
client = &http.Client{Timeout: 300 * time.Millisecond}
|
|
_, gerr := client.Get("http://" + pprofAddr + "/debug/pprof/")
|
|
if gerr == nil {
|
|
t.Error("expected pprof GET to fail after Shutdown")
|
|
}
|
|
_, merr := client.Get("http://" + mainAddr + "/healthz")
|
|
if merr == nil {
|
|
t.Error("expected main GET to fail after Shutdown")
|
|
}
|
|
}
|
|
|
|
// --- REQ-123 pprof loopback-only test ---
|
|
|
|
// TestStartPprof_NonLoopbackRefused verifies pprof refuses non-loopback.
|
|
func TestStartPprof_NonLoopbackRefused(t *testing.T) {
|
|
_, err := StartPprof("0.0.0.0:6060", slog.Default())
|
|
if err == nil {
|
|
t.Error("StartPprof on 0.0.0.0 should be refused (REQ-123)")
|
|
}
|
|
_, err = StartPprof("10.0.0.1:6060", slog.Default())
|
|
if err == nil {
|
|
t.Error("StartPprof on 10.0.0.1 should be refused (REQ-123)")
|
|
}
|
|
}
|
|
|
|
// TestStartPprof_LoopbackAccepted verifies loopback addresses are accepted.
|
|
func TestStartPprof_LoopbackAccepted(t *testing.T) {
|
|
srv, err := StartPprof("127.0.0.1:0", slog.Default())
|
|
if err != nil {
|
|
t.Fatalf("StartPprof on 127.0.0.1 should be accepted: %v", err)
|
|
}
|
|
if srv != nil {
|
|
srv.Close()
|
|
}
|
|
}
|
|
|
|
// TestStartPprof_EmptyHostRefused verifies that an address with an empty
|
|
// host (e.g. ":6060"), which binds ALL interfaces, is refused as
|
|
// non-loopback (F2: loopback-bypass fix).
|
|
func TestStartPprof_EmptyHostRefused(t *testing.T) {
|
|
_, err := StartPprof(":6060", slog.Default())
|
|
if err == nil {
|
|
t.Error("StartPprof on \":6060\" should be refused (F2: empty host binds all interfaces)")
|
|
}
|
|
if err != nil && !strings.Contains(err.Error(), "non-loopback") {
|
|
t.Errorf("error should mention non-loopback, got: %v", err)
|
|
}
|
|
}
|