99 lines
3.7 KiB
Go
99 lines
3.7 KiB
Go
package mitm
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// noopHandler is the simplest InterceptHandler that satisfies NewServer.
|
|
// We only exercise Start/Stop lifecycle here — no requests go through.
|
|
func noopHandler(_ http.ResponseWriter, _ *http.Request, _ string) {}
|
|
|
|
func newTestServer(addr string, hosts []string) *Server {
|
|
ca, err := NewInMemoryCA()
|
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "NewInMemoryCA")
|
|
srv, err := NewServer(Config{
|
|
Addr: addr,
|
|
CA: ca,
|
|
InterceptHosts: hosts,
|
|
Handler: noopHandler,
|
|
})
|
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "NewServer")
|
|
return srv
|
|
}
|
|
|
|
// Server_StopIdempotent: calling Stop twice (and Stop without
|
|
// Start) must not panic or deadlock. The application's RestartMITM
|
|
// path is sensitive to this — it always calls Stop before Start, even
|
|
// when the server is already stopped.
|
|
var _ = Describe("Server", func() {
|
|
It("Stop is idempotent", func() {
|
|
srv := newTestServer("127.0.0.1:0", nil)
|
|
srv.Stop() // never started
|
|
srv.Stop() // double-stop after never-started
|
|
|
|
srv2 := newTestServer("127.0.0.1:0", nil)
|
|
Expect(srv2.Start()).To(Succeed())
|
|
srv2.Stop()
|
|
srv2.Stop() // second Stop after Start+Stop
|
|
})
|
|
|
|
// Server_RestartCycle: two sequential Server lifecycles on the
|
|
// same address must rebind cleanly, the new listener must accept
|
|
// connections, and the new allowlist must take effect — the shape
|
|
// RestartMITM relies on.
|
|
It("restart cycle rebinds and swaps allowlist", func() {
|
|
// First, find a free port we can rebind to.
|
|
probe, err := net.Listen("tcp", "127.0.0.1:0")
|
|
Expect(err).NotTo(HaveOccurred(), "probe listen")
|
|
addr := probe.Addr().String()
|
|
_ = probe.Close()
|
|
|
|
srv1 := newTestServer(addr, []string{"first.example.com"})
|
|
if err := srv1.Start(); err != nil {
|
|
// Port could have been recycled between probe close and Start.
|
|
// Skip rather than flake — the production path uses dynamic
|
|
// addrs anyway.
|
|
Skip(fmt.Sprintf("could not bind probed addr: %v", err))
|
|
}
|
|
Expect(strings.HasPrefix(srv1.Addr(), "127.0.0.1:")).To(BeTrue(), "Addr() = %q, want 127.0.0.1:* prefix", srv1.Addr())
|
|
srv1.Stop()
|
|
|
|
// Now bring up a second server on the same addr with a different
|
|
// allowlist — mirrors the RestartMITM-with-edited-hosts path.
|
|
srv2 := newTestServer(addr, []string{"second.example.com"})
|
|
if err := srv2.Start(); err != nil {
|
|
// SO_REUSEADDR is not set; brief TIME_WAIT collisions are
|
|
// possible on slow CI runners. Retry once on a fresh port so
|
|
// the test still exercises the "different hosts" property.
|
|
srv2 = newTestServer("127.0.0.1:0", []string{"second.example.com"})
|
|
Expect(srv2.Start()).To(Succeed(), "second Start (fresh port fallback)")
|
|
}
|
|
defer srv2.Stop()
|
|
|
|
// Smoke: the new listener accepts a TCP connection.
|
|
conn, err := net.Dial("tcp", srv2.Addr())
|
|
Expect(err).NotTo(HaveOccurred(), "dial restarted listener")
|
|
_ = conn.Close()
|
|
|
|
// Allowlist swap took effect: the new server intercepts
|
|
// "second.example.com" (and not the old "first.example.com").
|
|
Expect(srv2.shouldIntercept("second.example.com")).To(BeTrue(), "second server did not pick up the new InterceptHosts")
|
|
Expect(srv2.shouldIntercept("first.example.com")).To(BeFalse(), "second server still has the first server's allowlist")
|
|
})
|
|
|
|
// Server_AddrBeforeStart: Addr() pre-Start returns the configured
|
|
// address rather than panicking on a nil listener. The admin status
|
|
// endpoint reads it under MITMServer() — when an admin queries between
|
|
// configuration and Start, the response should still render cleanly.
|
|
It("Addr before start returns configured address", func() {
|
|
srv := newTestServer(":12345", nil)
|
|
Expect(srv.Addr()).To(Equal(":12345"))
|
|
})
|
|
})
|