diff --git a/.changelog/ed6b34a441e64b47affa11a9f5066b23.json b/.changelog/ed6b34a441e64b47affa11a9f5066b23.json new file mode 100644 index 00000000000..cef5ffde982 --- /dev/null +++ b/.changelog/ed6b34a441e64b47affa11a9f5066b23.json @@ -0,0 +1,8 @@ +{ + "id": "ed6b34a4-41e6-4b47-affa-11a9f5066b23", + "type": "bugfix", + "description": "aws/signer/v4: Fixes a panic in SDK's handling of endpoint URLs with ports by correcting how URL path is parsed from opaque URLs. Fixes [#1294](https://github.com/aws/aws-sdk-go-v2/issues/1294).", + "modules": [ + "." + ] +} diff --git a/aws/signer/internal/v4/util.go b/aws/signer/internal/v4/util.go index 0cb9cffaf51..d025dbaa060 100644 --- a/aws/signer/internal/v4/util.go +++ b/aws/signer/internal/v4/util.go @@ -46,19 +46,35 @@ func StripExcessSpaces(str string) string { return string(buf[:m]) } -// GetURIPath returns the escaped URI component from the provided URL +// GetURIPath returns the escaped URI component from the provided URL. func GetURIPath(u *url.URL) string { - var uri string + var uriPath string if len(u.Opaque) > 0 { - uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/") + const schemeSep, pathSep, queryStart = "//", "/", "?" + + opaque := u.Opaque + // Cut off the query string if present. + if idx := strings.Index(opaque, queryStart); idx >= 0 { + opaque = opaque[:idx] + } + + // Cutout the scheme separator if present. + if strings.Index(opaque, schemeSep) == 0 { + opaque = opaque[len(schemeSep):] + } + + // capture URI path starting with first path separator. + if idx := strings.Index(opaque, pathSep); idx >= 0 { + uriPath = opaque[idx:] + } } else { - uri = u.EscapedPath() + uriPath = u.EscapedPath() } - if len(uri) == 0 { - uri = "/" + if len(uriPath) == 0 { + uriPath = "/" } - return uri + return uriPath } diff --git a/aws/signer/internal/v4/util_test.go b/aws/signer/internal/v4/util_test.go index c29c1fa8504..a38ef2d7402 100644 --- a/aws/signer/internal/v4/util_test.go +++ b/aws/signer/internal/v4/util_test.go @@ -1,9 +1,92 @@ package v4 import ( + "net/http" + "net/url" "testing" ) +func lazyURLParse(v string) func() (*url.URL, error) { + return func() (*url.URL, error) { + return url.Parse(v) + } +} + +func TestGetURIPath(t *testing.T) { + cases := map[string]struct { + getURL func() (*url.URL, error) + expect string + }{ + // Cases + "with scheme": { + getURL: lazyURLParse("https://localhost:9000"), + expect: "/", + }, + "no port, with scheme": { + getURL: lazyURLParse("https://localhost"), + expect: "/", + }, + "without scheme": { + getURL: lazyURLParse("localhost:9000"), + expect: "/", + }, + "without scheme, with path": { + getURL: lazyURLParse("localhost:9000/abc123"), + expect: "/abc123", + }, + "without scheme, with separator": { + getURL: lazyURLParse("//localhost:9000"), + expect: "/", + }, + "no port, without scheme, with separator": { + getURL: lazyURLParse("//localhost"), + expect: "/", + }, + "without scheme, with separator, with path": { + getURL: lazyURLParse("//localhost:9000/abc123"), + expect: "/abc123", + }, + "no port, without scheme, with separator, with path": { + getURL: lazyURLParse("//localhost/abc123"), + expect: "/abc123", + }, + "opaque with query string": { + getURL: lazyURLParse("localhost:9000/abc123?efg=456"), + expect: "/abc123", + }, + "failing test": { + getURL: func() (*url.URL, error) { + endpoint := "https://service.region.amazonaws.com" + req, _ := http.NewRequest("POST", endpoint, nil) + u := req.URL + + u.Opaque = "//example.org/bucket/key-._~,!@#$%^&*()" + + query := u.Query() + query.Set("some-query-key", "value") + u.RawQuery = query.Encode() + + return u, nil + }, + expect: "/bucket/key-._~,!@#$%^&*()", + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + u, err := c.getURL() + if err != nil { + t.Fatalf("failed to get URL, %v", err) + } + + actual := GetURIPath(u) + if e, a := c.expect, actual; e != a { + t.Errorf("expect %v path, got %v", e, a) + } + }) + } +} + func TestStripExcessHeaders(t *testing.T) { vals := []string{ "",