Files

170 lines
4.6 KiB
Go

package health
import (
"context"
"crypto/tls"
"crypto/x509"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
crawlv1alpha1 "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/api/v1alpha1"
)
// startConnectProxy runs a minimal but real HTTP proxy: CONNECT tunneling
// for https targets, absolute-URI forwarding for plain http ones. With
// refuseConnect it answers CONNECT with 502 — the "accepts TCP but cannot
// egress" failure mode the probe must classify as unhealthy.
func startConnectProxy(t *testing.T, refuseConnect bool) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
if refuseConnect {
http.Error(w, "no egress", http.StatusBadGateway)
return
}
dst, err := net.DialTimeout("tcp", r.Host, time.Second)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
conn, bufrw, err := http.NewResponseController(w).Hijack()
if err != nil {
_ = dst.Close()
t.Errorf("hijack: %v", err)
return
}
_, _ = bufrw.WriteString("HTTP/1.1 200 Connection established\r\n\r\n")
_ = bufrw.Flush()
done := make(chan struct{}, 2)
go func() { _, _ = io.Copy(dst, bufrw); done <- struct{}{} }()
go func() { _, _ = io.Copy(conn, dst); done <- struct{}{} }()
<-done
_ = conn.Close()
_ = dst.Close()
return
}
out := r.Clone(r.Context())
out.RequestURI = ""
resp, err := http.DefaultTransport.RoundTrip(out)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer func() { _ = resp.Body.Close() }()
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
}))
t.Cleanup(srv.Close)
return srv
}
func startTLSTarget(t *testing.T, status int) (*httptest.Server, *tls.Config) {
t.Helper()
target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(status)
}))
t.Cleanup(target.Close)
pool := x509.NewCertPool()
pool.AddCert(target.Certificate())
return target, &tls.Config{RootCAs: pool}
}
func proxyURL(t *testing.T, srv *httptest.Server) *url.URL {
t.Helper()
u, err := url.Parse(srv.URL)
if err != nil {
t.Fatalf("parsing proxy URL: %v", err)
}
return u
}
func testHC(probeTarget string) crawlv1alpha1.HealthCheckSpec {
return crawlv1alpha1.HealthCheckSpec{
ProbeURL: probeTarget,
IntervalSeconds: 30,
TimeoutSeconds: 5,
FailureThreshold: 3,
SuccessThreshold: 1,
ExpectedStatusCodes: []int32{200, 204},
}
}
func TestProbe_connectTunnelSucceeds(t *testing.T) {
t.Parallel()
target, tlsCfg := startTLSTarget(t, http.StatusNoContent)
proxy := startConnectProxy(t, false)
res := probe(context.Background(), proxyURL(t, proxy), testHC(target.URL), tlsCfg)
if !res.ok {
t.Fatalf("probe failed through working proxy: %v", res.err)
}
if res.latency <= 0 {
t.Errorf("latency = %v, want > 0", res.latency)
}
}
func TestProbe_refusedConnectFails(t *testing.T) {
t.Parallel()
target, tlsCfg := startTLSTarget(t, http.StatusNoContent)
proxy := startConnectProxy(t, true)
res := probe(context.Background(), proxyURL(t, proxy), testHC(target.URL), tlsCfg)
if res.ok {
t.Fatal("probe succeeded through a proxy that refuses CONNECT")
}
if res.err == nil {
t.Error("expected an error from the refused CONNECT")
}
}
func TestProbe_unexpectedStatusFails(t *testing.T) {
t.Parallel()
target, tlsCfg := startTLSTarget(t, http.StatusInternalServerError)
proxy := startConnectProxy(t, false)
res := probe(context.Background(), proxyURL(t, proxy), testHC(target.URL), tlsCfg)
if res.ok {
t.Fatal("probe succeeded on a 500 response")
}
}
func TestProbe_unreachableProxyFails(t *testing.T) {
t.Parallel()
// A listener that is immediately closed: guaranteed-refused port.
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("reserving port: %v", err)
}
dead := &url.URL{Scheme: "http", Host: l.Addr().String()}
_ = l.Close()
res := probe(context.Background(), dead, testHC("https://example.invalid/"), nil)
if res.ok {
t.Fatal("probe succeeded against a dead proxy")
}
}
func TestProbe_plainHTTPForwardSucceeds(t *testing.T) {
t.Parallel()
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(target.Close)
proxy := startConnectProxy(t, false)
res := probe(context.Background(), proxyURL(t, proxy), testHC(target.URL), nil)
if !res.ok {
t.Fatalf("plain-http probe failed: %v", res.err)
}
}