package credential import ( "context" "errors" "io" "net/http" "net/http/httptest" "net/netip" "strings" "testing" ) func TestTelegramProxyValidation(t *testing.T) { for _, raw := range []string{"", "http://127.0.0.1:9000", "http://[::1]:8080"} { if _, err := NormalizeTelegramProxyURL(raw); err != nil { t.Fatalf("valid proxy rejected: %q", raw) } } for _, raw := range []string{ "http://localhost:9000", "http://10.0.0.1:9000", "http://8.8.8.8:9000", "https://127.0.0.1:9000", "socks5://127.0.0.1:9000", "http://127.0.0.1", "http://127.0.0.1:0", "http://127.0.0.1:65536", "http://user:pass@127.0.0.1:9000", "http://127.0.0.1:9000/", "http://127.0.0.1:9000?", "http://127.0.0.1:9000#", "http://[::1%25lo]:9000", } { if _, err := NormalizeTelegramProxyURL(raw); err == nil { t.Fatalf("invalid proxy accepted: %q", raw) } } } func TestTelegramProxyRetainsTargetAndTLSRestrictions(t *testing.T) { client, err := newTelegramHTTPClient(30000, "http://127.0.0.1:9000", false) if err != nil { t.Fatal(err) } defer client.CloseIdleConnections() transport := client.Transport.(restrictedRoundTripper).transport.(*http.Transport) request := httptest.NewRequest(http.MethodPost, "https://api.telegram.org/bot-test/getMe", nil) proxy, err := transport.Proxy(request) if err != nil || proxy.String() != "http://127.0.0.1:9000" { t.Fatal("configured proxy was not applied") } if transport.TLSClientConfig != nil && transport.TLSClientConfig.InsecureSkipVerify { t.Fatal("TLS verification was disabled") } if connection, err := transport.DialContext(context.Background(), "tcp", "127.0.0.1:9001"); err == nil { connection.Close() t.Fatal("dial escaped configured proxy") } for _, target := range []string{"https://example.com/", "https://127.0.0.1/", "http://api.telegram.org/", "https://api.telegram.org:444/"} { request := httptest.NewRequest(http.MethodGet, target, nil) if _, err := client.Transport.RoundTrip(request); err == nil { t.Fatalf("unauthorized target accepted: %s", target) } if err := client.CheckRedirect(request, nil); err == nil { t.Fatalf("unauthorized redirect accepted: %s", target) } } } func TestTelegramSandboxNeverDialsProxyDirectly(t *testing.T) { client, err := newTelegramHTTPClient(30000, "http://127.0.0.1:9000", true) if err != nil { t.Fatal(err) } defer client.CloseIdleConnections() if _, ok := client.Transport.(restrictedRoundTripper).transport.(*pluginEgressTransport); !ok { t.Fatal("sandbox escaped host-managed egress") } } func TestTelegramProxyUsesConnectWithoutSendingTokenInPlaintext(t *testing.T) { requests := make(chan string, 1) proxy := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { requests <- request.Method + " " + request.RequestURI writer.WriteHeader(http.StatusForbidden) })) defer proxy.Close() client, err := newTelegramHTTPClient(1000, proxy.URL, false) if err != nil { t.Fatal(err) } defer client.CloseIdleConnections() if _, err := client.Get("https://api.telegram.org/bot-secret/getMe"); err == nil { t.Fatal("failed CONNECT was accepted") } select { case request := <-requests: if request != "CONNECT api.telegram.org:443" { t.Fatalf("unexpected proxy request: %s", request) } default: t.Fatal("proxy was not contacted") } } func TestTelegramEgressRejectsInvalidProxyWithoutFallback(t *testing.T) { token := strings.Repeat("a", 64) handler := NewPluginEgressHandlerWithTelegramProxy(token, func() []NetworkTarget { return []NetworkTarget{{Scheme: "https", Host: "api.telegram.org", Port: 443}} }, func() (string, error) { return "", errors.New("private configuration detail") }) request := httptest.NewRequest(http.MethodPost, "/relay", strings.NewReader("{}")) request.Host = "plugin-egress" request.Header.Set(egressTokenHeader, token) request.Header.Set(egressTargetHeader, "https://api.telegram.org/bot-secret/getMe") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusForbidden || strings.Contains(response.Body.String(), "private") { t.Fatal("invalid proxy did not fail closed with a sanitized error") } } func TestValidateSecret(t *testing.T) { value, err := ValidateSecret(" token-value ", 64) if err != nil || value != "token-value" { t.Fatal("valid credential was rejected") } for _, input := range []string{"", "line\nbreak"} { if _, err := ValidateSecret(input, 64); err == nil { t.Fatal("invalid credential was accepted") } } } func TestNormalizeHTTPSURL(t *testing.T) { value, err := NormalizeHTTPSURL("https://model.example.com/v1/predict?model=mail") if err != nil || value != "https://model.example.com/v1/predict?model=mail" { t.Fatal("valid HTTPS endpoint was rejected") } for _, input := range []string{ "http://model.example.com/v1/predict", "https://user:password@model.example.com/v1/predict", "https://model.example.com/v1/predict#fragment", "https:///v1/predict", "https://127.0.0.1/v1/predict", "https://10.0.0.1/v1/predict", "https://169.254.169.254/latest/meta-data", "https://100.64.0.1/v1/predict", "https://[::1]/v1/predict", "https://localhost/v1/predict", "https://service.localhost/v1/predict", } { if _, err := NormalizeHTTPSURL(input); err == nil { t.Fatalf("unsafe endpoint was accepted: %s", input) } } } func TestIsPublicRemoteIPRejectsReservedAddresses(t *testing.T) { for _, value := range []string{ "0.0.0.0", "100.64.0.1", "127.0.0.1", "169.254.0.1", "192.0.2.1", "198.18.0.1", "198.51.100.1", "203.0.113.1", "240.0.0.1", "::1", "64:ff9b::7f00:1", "100::1", "2001::1", "2001:db8::1", "2002:7f00:1::", "3fff::1", "4000::1", "fc00::1", "fe80::1", } { if IsPublicRemoteIP(netip.MustParseAddr(value)) { t.Fatalf("reserved address was accepted: %s", value) } } for _, value := range []string{"8.8.8.8", "1.1.1.1", "2606:4700:4700::1111"} { if !IsPublicRemoteIP(netip.MustParseAddr(value)) { t.Fatalf("public address was rejected: %s", value) } } } func TestRestrictedHTTPClientRejectsLocalEndpoint(t *testing.T) { server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNoContent) })) defer server.Close() client := NewRestrictedHTTPClient(1000) if _, err := client.Get(server.URL); err == nil { t.Fatal("restricted client connected to a local endpoint") } } func TestRestrictedHTTPClientRejectsPrivateRedirect(t *testing.T) { client := NewRestrictedHTTPClient(1000) request := httptest.NewRequest(http.MethodGet, "https://127.0.0.1/internal", nil) if err := client.CheckRedirect(request, nil); err == nil { t.Fatal("restricted client accepted a private redirect") } } func TestRestrictedHTTPClientRejectsCrossTargetRedirect(t *testing.T) { client := NewRestrictedHTTPClientForTargets(1000, []NetworkTarget{{Scheme: "https", Host: "allowed.example.com", Port: 443}}) request := httptest.NewRequest(http.MethodGet, "https://other.example.com/internal", nil) if err := client.CheckRedirect(request, nil); err == nil { t.Fatal("restricted client accepted a redirect outside the authorized target") } } func TestRestrictedHTTPClientRejectsInitialHTTPRequest(t *testing.T) { client := NewRestrictedHTTPClientForTargets(1000, []NetworkTarget{{Scheme: "https", Host: "allowed.example.com", Port: 443}}) if _, err := client.Get("http://allowed.example.com:443/internal"); err == nil { t.Fatal("restricted client accepted an initial HTTP request") } } func TestRestrictedHTTPClientRejectsInitialUnauthorizedPort(t *testing.T) { client := NewRestrictedHTTPClientForTargets(1000, []NetworkTarget{{Scheme: "https", Host: "allowed.example.com", Port: 443}}) if _, err := client.Get("https://allowed.example.com:8443/internal"); err == nil { t.Fatal("restricted client accepted an unauthorized initial port") } } func TestRestrictedHTTPClientAllowsAuthorizedRedirect(t *testing.T) { client := NewRestrictedHTTPClientForTargets(1000, []NetworkTarget{{Scheme: "https", Host: "allowed.example.com", Port: 443}}) request := httptest.NewRequest(http.MethodGet, "https://allowed.example.com/next", nil) if err := client.CheckRedirect(request, nil); err != nil { t.Fatalf("authorized redirect was rejected: %v", err) } } func TestNormalizeTimeoutMS(t *testing.T) { if NormalizeTimeoutMS(0) != defaultTimeoutMS || NormalizeTimeoutMS(1) != minTimeoutMS || NormalizeTimeoutMS(60000) != maxTimeoutMS { t.Fatal("timeout limit was not normalized") } } func TestPluginEgressRejectsInvalidRequests(t *testing.T) { token := strings.Repeat("a", 64) allowed := []NetworkTarget{{Scheme: "https", Host: "allowed.example.com", Port: 443}} cases := []struct { name string change func(*http.Request) status int }{ {"missing_token", func(r *http.Request) { r.Header.Del(egressTokenHeader) }, http.StatusForbidden}, {"wrong_token", func(r *http.Request) { r.Header.Set(egressTokenHeader, strings.Repeat("b", 64)) }, http.StatusForbidden}, {"duplicate_token", func(r *http.Request) { r.Header.Add(egressTokenHeader, token) }, http.StatusForbidden}, {"wrong_host", func(r *http.Request) { r.Host = "other" }, http.StatusBadRequest}, {"wrong_path", func(r *http.Request) { r.URL.Path = "/other" }, http.StatusBadRequest}, {"query", func(r *http.Request) { r.URL.RawQuery = "x=1" }, http.StatusBadRequest}, {"connect", func(r *http.Request) { r.Method = http.MethodConnect }, http.StatusBadRequest}, {"upgrade", func(r *http.Request) { r.Header.Set("Upgrade", "websocket") }, http.StatusBadRequest}, {"duplicate_target", func(r *http.Request) { r.Header.Add(egressTargetHeader, "https://allowed.example.com") }, http.StatusForbidden}, {"http_target", func(r *http.Request) { r.Header.Set(egressTargetHeader, "http://allowed.example.com") }, http.StatusForbidden}, {"private_target", func(r *http.Request) { r.Header.Set(egressTargetHeader, "https://127.0.0.1") }, http.StatusForbidden}, {"other_target", func(r *http.Request) { r.Header.Set(egressTargetHeader, "https://other.example.com") }, http.StatusForbidden}, {"other_port", func(r *http.Request) { r.Header.Set(egressTargetHeader, "https://allowed.example.com:8443") }, http.StatusForbidden}, {"oversized_body", func(r *http.Request) { r.ContentLength = maxEgressRequestBytes + 1 }, http.StatusRequestEntityTooLarge}, } for _, item := range cases { t.Run(item.name, func(t *testing.T) { request := httptest.NewRequest(http.MethodPost, "/relay", nil) request.Host = "plugin-egress" request.Header.Set(egressTokenHeader, token) request.Header.Set(egressTargetHeader, "https://allowed.example.com/send") item.change(request) response := httptest.NewRecorder() NewPluginEgressHandler(token, func() []NetworkTarget { return allowed }).ServeHTTP(response, request) if response.Code != item.status { t.Fatalf("unexpected rejection status: %d", response.Code) } }) } } func TestPluginEgressEmptyAuthorizationFailsClosed(t *testing.T) { for _, authorize := range []func() []NetworkTarget{ nil, func() []NetworkTarget { return nil }, func() []NetworkTarget { return []NetworkTarget{{Scheme: "invalid", Host: "allowed.example.com"}} }, } { token := strings.Repeat("a", 64) request := httptest.NewRequest(http.MethodPost, "/relay", nil) request.Host = "plugin-egress" request.Header.Set(egressTokenHeader, token) request.Header.Set(egressTargetHeader, "https://allowed.example.com/send") response := httptest.NewRecorder() NewPluginEgressHandler(token, authorize).ServeHTTP(response, request) if response.Code != http.StatusForbidden { t.Fatalf("empty authorization was accepted: %d", response.Code) } } } func TestPluginEgressRechecksAuthorizationBeforeOutbound(t *testing.T) { token := strings.Repeat("a", 64) calls := 0 handler := NewPluginEgressHandler(token, func() []NetworkTarget { calls++ if calls > 1 { return nil } return []NetworkTarget{{Scheme: "https", Host: "allowed.example.com", Port: 443}} }) request := httptest.NewRequest(http.MethodPost, "/relay", strings.NewReader("payload")) request.Host = "plugin-egress" request.Header.Set(egressTokenHeader, token) request.Header.Set(egressTargetHeader, "https://allowed.example.com/send") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusForbidden || calls != 2 { t.Fatalf("authorization was not rechecked: status=%d calls=%d", response.Code, calls) } } func TestStripEgressHeaders(t *testing.T) { header := make(http.Header) for _, name := range []string{egressTokenHeader, egressTargetHeader, "Proxy-Authorization", "X-Hop", "Upgrade"} { header.Set(name, "sensitive") } header.Set("Connection", "X-Hop, Upgrade") header.Set("Content-Type", "application/json") stripEgressHeaders(header) if len(header) != 1 || header.Get("Content-Type") != "application/json" { t.Fatal("internal or hop-by-hop headers survived filtering") } } type egressTrackedBody struct { io.Reader closed bool } func (b *egressTrackedBody) Close() error { b.closed = true return nil } func TestRestrictedTransportsCloseRejectedRequestBody(t *testing.T) { for _, transport := range []http.RoundTripper{restrictedRoundTripper{}, &pluginEgressTransport{}} { body := &egressTrackedBody{Reader: strings.NewReader("payload")} request := httptest.NewRequest(http.MethodPost, "http://127.0.0.1/", body) if _, err := transport.RoundTrip(request); err == nil || !body.closed { t.Fatal("rejected request body was not closed") } } } type egressIdleTransport struct { http.RoundTripper closed bool } func (t *egressIdleTransport) CloseIdleConnections() { t.closed = true } func TestRestrictedTransportForwardsIdleConnectionCleanup(t *testing.T) { transport := &egressIdleTransport{} client := &http.Client{Transport: restrictedRoundTripper{transport: &pluginEgressTransport{transport: transport}}} client.CloseIdleConnections() if !transport.closed { t.Fatal("idle connection cleanup was not forwarded") } }