135 lines
4.2 KiB
Go
135 lines
4.2 KiB
Go
package config
|
|
|
|
// HTTP_TRUSTED_PROXIES parsing.
|
|
//
|
|
// The setting decides whether a client-supplied header is believed, so the
|
|
// tests worth having are about what happens when it is WRONG: unset, empty,
|
|
// mistyped. Every one of those must end in "trust nothing", because the
|
|
// alternative — trusting something the operator did not write — is the whole
|
|
// risk this setting carries.
|
|
|
|
import (
|
|
"net/netip"
|
|
"testing"
|
|
)
|
|
|
|
func TestTrustedProxiesUnsetTrustsNothing(t *testing.T) {
|
|
for _, raw := range []string{"", " ", ",", " , , "} {
|
|
got, err := parseTrustedProxies(raw)
|
|
if err != nil {
|
|
t.Errorf("parseTrustedProxies(%q): unexpected error %v", raw, err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Errorf("parseTrustedProxies(%q) = %v, want empty — an unset value must trust nothing", raw, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxiesParsesCIDRsAndBareAddresses(t *testing.T) {
|
|
got, err := parseTrustedProxies(" 10.0.0.0/8 , 172.17.0.1 , fd00::/8 , ::1 ")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
want := []string{"10.0.0.0/8", "172.17.0.1/32", "fd00::/8", "::1/128"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("parsed %d entries (%v), want %d", len(got), got, len(want))
|
|
}
|
|
for i, w := range want {
|
|
if got[i].String() != w {
|
|
t.Errorf("entry %d = %q, want %q", i, got[i].String(), w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A bare address must become a single-host block that contains that host and
|
|
// nothing else — the operator wrote one proxy, not a network.
|
|
func TestTrustedProxyBareAddressIsOneHost(t *testing.T) {
|
|
got, err := parseTrustedProxies("172.17.0.1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !got[0].Contains(netip.MustParseAddr("172.17.0.1")) {
|
|
t.Error("the host itself is not in its own single-host block")
|
|
}
|
|
if got[0].Contains(netip.MustParseAddr("172.17.0.2")) {
|
|
t.Error("a bare address was widened beyond one host")
|
|
}
|
|
}
|
|
|
|
// A block written with host bits set is common and easy to misread. Masking it
|
|
// at parse time makes it mean what its author meant; unmasked, netip.Prefix
|
|
// .Contains reports false for everything.
|
|
func TestTrustedProxyCIDRWithHostBitsIsMasked(t *testing.T) {
|
|
got, err := parseTrustedProxies("10.1.2.3/8")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if want := "10.0.0.0/8"; got[0].String() != want {
|
|
t.Fatalf("got %q, want %q", got[0].String(), want)
|
|
}
|
|
if !got[0].Contains(netip.MustParseAddr("10.9.9.9")) {
|
|
t.Error("the masked block does not contain an address inside it")
|
|
}
|
|
}
|
|
|
|
// An IPv4-mapped address names an IPv4 host, and must match the peer address
|
|
// Go reports for an IPv4 connection.
|
|
func TestTrustedProxyIPv4MappedIsUnmapped(t *testing.T) {
|
|
got, err := parseTrustedProxies("::ffff:10.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !got[0].Contains(netip.MustParseAddr("10.0.0.1")) {
|
|
t.Errorf("%q does not contain 10.0.0.1", got[0].String())
|
|
}
|
|
}
|
|
|
|
// Malformed entries stop startup. Skipping one would leave the deployment
|
|
// trusting a shorter list than the operator wrote, and the consequence — every
|
|
// user sharing one rate-limit bucket — is silent.
|
|
func TestTrustedProxiesRejectMalformedEntries(t *testing.T) {
|
|
for _, raw := range []string{
|
|
"banana",
|
|
"10.0.0.0/33",
|
|
"10.0.0.0/8, banana",
|
|
"300.1.2.3",
|
|
"10.0.0.1:8080",
|
|
"*",
|
|
"https://proxy.internal",
|
|
"fd00::/200",
|
|
} {
|
|
if _, err := parseTrustedProxies(raw); err == nil {
|
|
t.Errorf("parseTrustedProxies(%q) was accepted; it must refuse and stop startup", raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The error has to name the entry and show the shape expected, because it is
|
|
// read by an operator at 3am with a container that will not boot.
|
|
func TestTrustedProxiesErrorNamesTheEntry(t *testing.T) {
|
|
_, err := parseTrustedProxies("10.0.0.0/8, banana")
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
for _, want := range []string{"HTTP_TRUSTED_PROXIES", "banana"} {
|
|
if !contains(err.Error(), want) {
|
|
t.Errorf("error %q does not mention %q", err.Error(), want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func contains(haystack, needle string) bool {
|
|
return len(haystack) >= len(needle) && (haystack == needle ||
|
|
len(needle) == 0 || indexOf(haystack, needle) >= 0)
|
|
}
|
|
|
|
func indexOf(haystack, needle string) int {
|
|
for i := 0; i+len(needle) <= len(haystack); i++ {
|
|
if haystack[i:i+len(needle)] == needle {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|