diff --git a/internal/handlers/image_errors_internal_test.go b/internal/handlers/image_errors_internal_test.go index fbbcf6f..27f1d4f 100644 --- a/internal/handlers/image_errors_internal_test.go +++ b/internal/handlers/image_errors_internal_test.go @@ -264,3 +264,75 @@ func TestHandleImage_AllowlistOrSignature(t *testing.T) { }) } } + +// TestHandleImage_InvalidHost_Returns400 sends image URLs whose host is +// allowlistedHost with a character after it that a URL host cannot hold, +// percent-encoded, and checks that each is refused with 400 naming the host +// as decoded. A "?" or "#" there would otherwise end the host when the source +// URL is read, so the request would pass the allowlist and be fetched and +// cached under a host that is not the one fetched. +func TestHandleImage_InvalidHost_Returns400(t *testing.T) { + t.Parallel() + + photos := newPhotoFetcher(t, allowlistedHost) + + tests := []struct { + name string + writtenHost string + decodedHost string + }{ + {"? written as %3F", allowlistedHost + "%3Fx=1", allowlistedHost + "?x=1"}, + {"# written as %23", allowlistedHost + "%23x", allowlistedHost + "#x"}, + {"% written as %25", allowlistedHost + "%253F", allowlistedHost + "%3F"}, + {`\ written as %5C`, allowlistedHost + "%5Cx", allowlistedHost + `\x`}, + {"space written as %20", allowlistedHost + "%20", allowlistedHost + " "}, + {"IPv6 address without brackets", "2001:db8::1", "2001:db8::1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + rec := sendGet(t, newImageRoute(t, photos), photoURL(tt.writtenHost)) + checkErrorBody(t, rec, http.StatusBadRequest, + fmt.Sprintf("invalid image URL: invalid source host: %q", + tt.decodedHost)) + }) + } +} + +// TestHandleImage_HostForms_Served checks that the image route serves, with +// a valid signature, the image at photoPath on a host name, an IPv4 address, +// an IPv6 address in brackets, and a host followed by ":" with or without a +// port. +func TestHandleImage_HostForms_Served(t *testing.T) { + t.Parallel() + + hosts := []string{ + allowlistedHost, + "img_1.example.com", + "203.0.113.5", + "[2001:db8::1]", + allowlistedHost + ":8443", + "[2001:db8::1]:8443", + allowlistedHost + ":", + } + + photos := newPhotoFetcher(t, hosts...) + exp := time.Now().Add(time.Hour) + + for _, host := range hosts { + t.Run(host, func(t *testing.T) { + t.Parallel() + + sig := photoSignature(testSigningKey, host, exp) + + rec := sendGet(t, newImageRoute(t, photos), + photoURLWithSig(host, sig, exp)) + if rec.Code != http.StatusOK { + t.Errorf("status = %d, want %d: %s", + rec.Code, http.StatusOK, rec.Body) + } + }) + } +}