Fetch an IPv6 source host written without a port (closes #295)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The fetcher took the host from the source URL and stripped a port with net.SplitHostPort, which fails when there is no port, so a bracketed IPv6 host such as [2001:db8::a] kept its brackets, its name lookup failed, and the request answered 500. validateURL now takes the host with url.URL.Hostname(), which drops the brackets and any port, so such a host is fetched like one with a port, through /v1/image/ and through an encrypted URL, and a private IPv6 address is still refused. Model: opus-5-5
This commit was merged in pull request #296.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user