diff --git a/internal/ogtags/ogtags.go b/internal/ogtags/ogtags.go index 68fb164c..fad10255 100644 --- a/internal/ogtags/ogtags.go +++ b/internal/ogtags/ogtags.go @@ -21,6 +21,24 @@ const ( querySeparatorLength = 1 // Length of "?" for query strings ) +// unixRoundTripper wraps an http.Transport to handle Unix socket requests properly +// by ensuring the URL scheme is set to "http" to avoid TLS issues. +// Based on the UnixRoundTripper implementation in lib/http.go +type unixRoundTripper struct { + Transport *http.Transport +} + +// RoundTrip implements the http.RoundTripper interface +func (t *unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + req = req.Clone(req.Context()) + if req.Host == "" { + req.Host = "localhost" + } + req.URL.Host = req.Host + req.URL.Scheme = "http" // Ensure HTTP scheme to avoid "server gave HTTP response to HTTPS client" error + return t.Transport.RoundTrip(req) +} + type OGTagCache struct { cache store.JSON[map[string]string] targetURL *url.URL @@ -69,11 +87,12 @@ func NewOGTagCache(target string, conf config.OpenGraph, backend store.Interface // Configure custom transport for Unix sockets if parsedTargetURL.Scheme == "unix" { socketPath := parsedTargetURL.Path // For unix scheme, path is the socket path - client.Transport = &http.Transport{ + transport := &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", socketPath) }, } + client.Transport = &unixRoundTripper{Transport: transport} } return &OGTagCache{ diff --git a/internal/ogtags/ogtags_test.go b/internal/ogtags/ogtags_test.go index c936b015..f8b2984a 100644 --- a/internal/ogtags/ogtags_test.go +++ b/internal/ogtags/ogtags_test.go @@ -99,16 +99,16 @@ func TestNewOGTagCache_UnixSocket(t *testing.T) { } // Check if the client transport is configured for Unix sockets - transport, ok := cache.client.Transport.(*http.Transport) + roundTripper, ok := cache.client.Transport.(*unixRoundTripper) if !ok { - t.Fatalf("expected client transport to be *http.Transport, got %T", cache.client.Transport) + t.Fatalf("expected client transport to be *unixRoundTripper, got %T", cache.client.Transport) } - if transport.DialContext == nil { + if roundTripper.Transport.DialContext == nil { t.Fatal("expected client transport DialContext to be non-nil for unix socket") } // Attempt a dummy dial to see if it uses the correct path (optional, more involved check) - dummyConn, err := transport.DialContext(context.Background(), "", "") + dummyConn, err := roundTripper.Transport.DialContext(context.Background(), "", "") if err == nil { dummyConn.Close() t.Log("DialContext seems functional, but couldn't verify path without a listener") diff --git a/internal/ogtags/test_https_issue_test.go b/internal/ogtags/test_https_issue_test.go deleted file mode 100644 index c65f6d97..00000000 --- a/internal/ogtags/test_https_issue_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package ogtags - -import ( - "net/url" - "testing" - "time" - - "github.com/TecharoHQ/anubis/lib/policy/config" - "github.com/TecharoHQ/anubis/lib/store/memory" -) - -// TestUnixSocketHTTPSIssue reproduces the issue where unix socket targets -// might cause HTTPS/HTTP mismatch errors -func TestUnixSocketHTTPSIssue(t *testing.T) { - target := "unix:///var/run/app.sock" - - cache := NewOGTagCache(target, config.OpenGraph{ - Enabled: true, - TimeToLive: time.Minute, - }, memory.New(t.Context())) - - // Test with HTTPS URL (this might be the source of confusion) - httpsURL, _ := url.Parse("https://example.com/test?param=value") - - // Get the target URL that would be used for the request - targetURL := cache.getTarget(httpsURL) - - t.Logf("Target URL for unix socket: %s", targetURL) - - // The target URL should be using http:// scheme, not https:// - expected := "http://unix/test?param=value" - if targetURL != expected { - t.Errorf("Expected %s, got %s", expected, targetURL) - } -} \ No newline at end of file diff --git a/internal/ogtags/test_tls_issue_test.go b/internal/ogtags/test_tls_issue_test.go deleted file mode 100644 index 3ff9585c..00000000 --- a/internal/ogtags/test_tls_issue_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package ogtags - -import ( - "context" - "net" - "net/http" - "net/url" - "os" - "path/filepath" - "testing" - "time" - - "github.com/TecharoHQ/anubis/lib/policy/config" - "github.com/TecharoHQ/anubis/lib/store/memory" -) - -// TestUnixSocketTLSIssue tries to reproduce the actual "http: server gave HTTP response to HTTPS client" error -func TestUnixSocketTLSIssue(t *testing.T) { - tempDir := t.TempDir() - socketPath := filepath.Join(tempDir, "test.sock") - - // Create a simple HTTP server listening on the Unix socket - server := &http.Server{ - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - w.Write([]byte(`
`)) - }), - } - - // Listen on Unix socket - listener, err := net.Listen("unix", socketPath) - if err != nil { - t.Fatalf("Failed to create Unix socket listener: %v", err) - } - defer os.Remove(socketPath) - defer listener.Close() - - // Start the server - go func() { - server.Serve(listener) - }() - defer server.Close() - - // Wait a bit for server to start - time.Sleep(100 * time.Millisecond) - - // Create OGTagCache with Unix socket target - target := "unix://" + socketPath - cache := NewOGTagCache(target, config.OpenGraph{ - Enabled: true, - TimeToLive: time.Minute, - }, memory.New(t.Context())) - - // Test with an HTTPS URL (this simulates the original request coming via HTTPS) - httpsURL, _ := url.Parse("https://example.com/test") - - // Try to get OG tags - this should work without HTTPS/HTTP errors - ogTags, err := cache.GetOGTags(context.Background(), httpsURL, "example.com") - if err != nil { - // If we get "http: server gave HTTP response to HTTPS client", this is the bug - if err.Error() == "http: server gave HTTP response to HTTPS client" { - t.Errorf("Found the bug: %v", err) - } else { - t.Logf("Got different error: %v", err) - } - } else { - t.Logf("Success: got OG tags: %v", ogTags) - } -} \ No newline at end of file diff --git a/internal/ogtags/test_debug_unix_test.go b/internal/ogtags/test_unix_https_fix_test.go similarity index 54% rename from internal/ogtags/test_debug_unix_test.go rename to internal/ogtags/test_unix_https_fix_test.go index 7164695e..01da134c 100644 --- a/internal/ogtags/test_debug_unix_test.go +++ b/internal/ogtags/test_unix_https_fix_test.go @@ -15,18 +15,22 @@ import ( "github.com/TecharoHQ/anubis/lib/store/memory" ) -// TestDebugUnixSocketRequests - let's debug exactly what URLs are being constructed -func TestDebugUnixSocketRequests(t *testing.T) { +// TestUnixSocketHTTPSFix tests that the fix prevents "http: server gave HTTP response to HTTPS client" errors +// when using Unix socket targets with HTTPS input URLs +func TestUnixSocketHTTPSFix(t *testing.T) { tempDir := t.TempDir() socketPath := filepath.Join(tempDir, "test.sock") - // Create a simple HTTP server listening on the Unix socket that logs requests + // Create a simple HTTP server listening on the Unix socket server := &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Logf("Received request: %s %s", r.Method, r.URL.String()) - t.Logf("Request scheme: %s", r.URL.Scheme) - t.Logf("Request host: %s", r.Host) - t.Logf("Is TLS: %v", r.TLS != nil) + // Verify that the request comes in with HTTP (not HTTPS) + if r.URL.Scheme != "" && r.URL.Scheme != "http" { + t.Errorf("Unexpected scheme in request: %s (expected 'http' or empty)", r.URL.Scheme) + } + if r.TLS != nil { + t.Errorf("Request has TLS information when it shouldn't for Unix socket") + } w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusOK) @@ -53,47 +57,41 @@ func TestDebugUnixSocketRequests(t *testing.T) { // Create OGTagCache with Unix socket target target := "unix://" + socketPath - t.Logf("Using target: %s", target) - cache := NewOGTagCache(target, config.OpenGraph{ Enabled: true, TimeToLive: time.Minute, }, memory.New(t.Context())) - // Test with various URL schemes + // Test cases that previously might have caused the "HTTP response to HTTPS client" error testCases := []struct { name string - inputURL string + url string }{ {"HTTPS URL", "https://example.com/test"}, - {"HTTP URL", "http://example.com/test"}, - {"HTTPS with port", "https://example.com:8080/test"}, + {"HTTPS with port", "https://example.com:443/test"}, + {"HTTPS with query", "https://example.com/test?param=value"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - inputURL, _ := url.Parse(tc.inputURL) - t.Logf("Input URL: %s", inputURL.String()) + inputURL, _ := url.Parse(tc.url) - // Get the target URL that will be used - targetURL := cache.getTarget(inputURL) - t.Logf("Target URL: %s", targetURL) - - // Verify that the target URL uses http scheme - if !strings.HasPrefix(targetURL, "http://unix") { - t.Errorf("Expected target URL to start with 'http://unix', got: %s", targetURL) - } - - // Try to get OG tags + // This should succeed without the "server gave HTTP response to HTTPS client" error ogTags, err := cache.GetOGTags(context.Background(), inputURL, "example.com") + if err != nil { if strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") { - t.Errorf("BUG FOUND: %v", err) + t.Errorf("Fix did not work: still getting HTTPS/HTTP error: %v", err) } else { - t.Logf("Different error: %v", err) + // Other errors are acceptable for this test + t.Logf("Got non-HTTPS error (acceptable): %v", err) } } else { - t.Logf("Success: got OG tags: %v", ogTags) + // Success case + if ogTags["og:title"] != "Test Title" { + t.Errorf("Expected og:title 'Test Title', got: %v", ogTags["og:title"]) + } + t.Logf("Success: got expected og:title = %s", ogTags["og:title"]) } }) }