diff --git a/internal/server/source_url_internal_test.go b/internal/server/source_url_internal_test.go index d006c44..0a5b4a1 100644 --- a/internal/server/source_url_internal_test.go +++ b/internal/server/source_url_internal_test.go @@ -6,12 +6,16 @@ import ( "net/url" "regexp" "slices" + "strconv" "strings" "sync" "testing" + "time" + "sneak.berlin/go/pixa/internal/encurl" "sneak.berlin/go/pixa/internal/imgcache" "sneak.berlin/go/pixa/internal/session" + "sneak.berlin/go/pixa/internal/signature" ) // percentSignPath is a source path holding a "%", which a URL writes %25. @@ -433,3 +437,109 @@ func TestGeneratorRefusesSourceURLItCannotFetchAsWritten(t *testing.T) { }) } } + +// ipv6SourcePath is the source path of the image URLs signedImageURL and +// encryptedImageURL make. +const ipv6SourcePath = "/ipv6.png" + +// TestImageRoutesFetchIPv6SourceHostWithOrWithoutPort requests ipv6SourcePath +// on an IPv6 source host, written with and without a port, through a +// /v1/image/ URL with a signature and through an encrypted URL. The public +// address 2001:db8::a, a documentation address (RFC 3849) the fetcher accepts +// as public, answers 200, and the upstream receives the request for +// ipv6SourcePath. The private address fd00::1 answers 403, and the upstream +// receives no request: the test's fetcher connects every address to the test +// upstream, so only the fetcher's check of the source URL can refuse it. +func TestImageRoutesFetchIPv6SourceHostWithOrWithoutPort(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + sourceHost string + wantStatus int + // wantRequests are the request targets the upstream must receive + wantRequests []string + }{ + {"public address without a port", "[2001:db8::a]", + http.StatusOK, []string{ipv6SourcePath}}, + {"public address with a port", "[2001:db8::a]:8443", + http.StatusOK, []string{ipv6SourcePath}}, + {"private address without a port", "[fd00::1]", + http.StatusForbidden, nil}, + {"private address with a port", "[fd00::1]:8443", + http.StatusForbidden, nil}, + } + + for _, tt := range tests { + imageURLs := map[string]string{ + "image URL with a signature": signedImageURL(t, tt.sourceHost), + "encrypted URL": encryptedImageURL(t, tt.sourceHost), + } + + for route, imageURL := range imageURLs { + t.Run(tt.name+" "+route, func(t *testing.T) { + t.Parallel() + + upstream := startRecordingUpstream(t) + s, _, _ := startImageProxy(t, upstream.Server) + + rec := httptest.NewRecorder() + s.ServeHTTP(rec, httptest.NewRequestWithContext( + t.Context(), http.MethodGet, imageURL, nil)) + t.Logf("GET %s: %d", imageURL, rec.Code) + + if rec.Code != tt.wantStatus { + t.Errorf("status = %d, want %d; body %s", + rec.Code, tt.wantStatus, rec.Body.String()) + } + + got := upstream.requestTargets() + t.Logf("upstream received %q", got) + + if !slices.Equal(got, tt.wantRequests) { + t.Errorf("upstream received %q, want %q", got, tt.wantRequests) + } + }) + } + } +} + +// signedImageURL returns the /v1/image/ URL of ipv6SourcePath on sourceHost at +// its original size as PNG, with a signature made with the test signing key, +// which a host not on the allowlist needs. The URL has no q or fit, so it is +// signed with the quality 85 and the fit cover. +func signedImageURL(t *testing.T, sourceHost string) string { + t.Helper() + + path, sig, exp := signature.New(testSigningKey).GenerateSignedURL( + &signature.Request{ + SourceHost: sourceHost, + SourcePath: ipv6SourcePath, + Format: string(imgcache.FormatPNG), + Quality: 85, + FitMode: string(imgcache.FitCover), + }, time.Hour) + + return path + "?sig=" + sig + "&exp=" + strconv.FormatInt(exp, 10) +} + +// encryptedImageURL returns the encrypted URL of ipv6SourcePath on sourceHost +// at its original size as PNG, made with the test signing key. +func encryptedImageURL(t *testing.T, sourceHost string) string { + t.Helper() + + generator, err := encurl.NewGenerator(testSigningKey) + if err != nil { + t.Fatalf("encurl.NewGenerator: %v", err) + } + + token, err := generator.Generate(&encurl.Payload{ + SourceHost: sourceHost, SourcePath: ipv6SourcePath, + Format: imgcache.FormatPNG, + }) + if err != nil { + t.Fatalf("encurl Generate: %v", err) + } + + return "/v1/e/" + token + "/img.png" +}