package httpapi import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestClientIP(t *testing.T) { cases := []struct { name string remoteAddr string headers map[string]string want string wantErr bool }{ {name: "ipv4", remoteAddr: "90.156.213.5:51234", want: "90.156.213.5"}, {name: "ipv4 mapped in ipv6", remoteAddr: "[::ffff:90.156.213.5]:51234", want: "90.156.213.5"}, {name: "ipv6", remoteAddr: "[2001:db8::7]:443", want: "2001:db8::7"}, // A client-supplied header must never change the answer. {name: "forwarded for is ignored", remoteAddr: "90.156.213.5:1", headers: map[string]string{"X-Forwarded-For": "1.2.3.4", "X-Real-IP": "5.6.7.8"}, want: "90.156.213.5"}, {name: "no port", remoteAddr: "90.156.213.5", wantErr: true}, {name: "not an address", remoteAddr: "host:80", wantErr: true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/", nil) r.RemoteAddr = tc.remoteAddr for k, v := range tc.headers { r.Header.Set(k, v) } got, err := clientIP(r) if tc.wantErr { if err == nil { t.Fatalf("expected an error, got %q", got) } return } if err != nil || got != tc.want { t.Fatalf("clientIP = %q, %v; want %q", got, err, tc.want) } }) } } // The route is open (no token), answers a known validator with the address // control-api sees, and refuses unknown validators and a forged header. func TestObservedIPEndpoint(t *testing.T) { fc, d, _, _ := newConfigTestHarness(t) if err := d.RegisterValidator(context.Background(), "validator-1", "host-1", "port-1", "v0.1"); err != nil { t.Fatal(err) } req, _ := http.NewRequest(http.MethodGet, fc.base+"/api/v1/agents/validator-1/observed-ip", nil) req.Header.Set("X-Forwarded-For", "8.8.8.8") resp, err := fc.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } var got observedIPResponse if err := json.NewDecoder(resp.Body).Decode(&got); err != nil { t.Fatal(err) } // httptest connects from loopback; the forged header must not leak through. if got.IP != "127.0.0.1" || got.Source != "remote_addr" { t.Fatalf("response = %+v, want 127.0.0.1 / remote_addr", got) } if resp, _ := fc.do(http.MethodGet, "/api/v1/agents/no-such-validator/observed-ip", nil); resp.StatusCode != http.StatusNotFound { t.Fatalf("unknown validator: status = %d, want 404", resp.StatusCode) } }