diff --git a/util/helm/client.go b/util/helm/client.go index 7ef14e2cba36c..ec0c0bea4dbaa 100644 --- a/util/helm/client.go +++ b/util/helm/client.go @@ -390,10 +390,10 @@ func getTagsListURL(rawURL string, chart string) (string, error) { if err != nil { return "", fmt.Errorf("unable to parse repo url: %v", err) } + tagsPathFormat := "%s/v2/%s/tags/list" repoURL.Scheme = "https" - tagsList := strings.Join([]string{"v2", url.PathEscape(chart), "tags/list"}, "/") - repoURL.Path = strings.Join([]string{repoURL.Path, tagsList}, "/") - repoURL.RawPath = strings.Join([]string{repoURL.RawPath, tagsList}, "/") + repoURL.Path = fmt.Sprintf(tagsPathFormat, repoURL.Path, chart) + repoURL.RawPath = fmt.Sprintf(tagsPathFormat, repoURL.RawPath, url.PathEscape(chart)) return repoURL.String(), nil } @@ -406,7 +406,7 @@ func (c *nativeHelmChart) getTags(chart string) ([]byte, error) { allTags := &TagsList{} var data []byte for nextURL != "" { - log.Debugf("fetching %s tags from %s", chart, text.Trunc(nextURL, 100)) + log.Debugf("fetching %s tags from %s", chart, sanitizeLog(text.Trunc(nextURL, 100))) data, nextURL, err = c.getTagsFromUrl(nextURL) if err != nil { return nil, fmt.Errorf("failed tags part: %v", err) @@ -426,14 +426,30 @@ func (c *nativeHelmChart) getTags(chart string) ([]byte, error) { return data, nil } -func getNextUrl(linkHeader string) string { - nextUrl := "" - if linkHeader != "" { - // drop < >; ref= from the Link header, see: https://docs.docker.com/registry/spec/api/#pagination - nextUrl = strings.Split(linkHeader, ";")[0][1:] - nextUrl = nextUrl[:len(nextUrl)-1] +func getNextUrl(resp *http.Response) (string, error) { + link := resp.Header.Get("Link") + if link == "" { + return "", nil } - return nextUrl + if link[0] != '<' { + return "", fmt.Errorf("invalid next link %q: missing '<'", link) + } + if i := strings.IndexByte(link, '>'); i == -1 { + return "", fmt.Errorf("invalid next link %q: missing '>'", link) + } else { + link = link[1:i] + } + linkURL, err := resp.Request.URL.Parse(link) + if err != nil { + return "", err + } + return linkURL.String(), nil +} + +func sanitizeLog(input string) string { + sanitized := strings.ReplaceAll(input, "\r", "") + sanitized = strings.ReplaceAll(sanitized, "\n", "") + return sanitized } func (c *nativeHelmChart) getTagsFromUrl(tagsURL string) ([]byte, string, error) { @@ -484,8 +500,8 @@ func (c *nativeHelmChart) getTagsFromUrl(tagsURL string) ([]byte, string, error) if err != nil { return nil, "", fmt.Errorf("failed to read body: %v", err) } - nextUrl := getNextUrl(resp.Header.Get("Link")) - return data, nextUrl, nil + nextUrl, err := getNextUrl(resp) + return data, nextUrl, err } func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error) { diff --git a/util/helm/client_test.go b/util/helm/client_test.go index 8f12eaf403c9e..8107a586e2772 100644 --- a/util/helm/client_test.go +++ b/util/helm/client_test.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httptest" + "net/url" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -177,11 +178,34 @@ func TestGetTagsFromUrl(t *testing.T) { } func Test_getNextUrl(t *testing.T) { - nextUrl := getNextUrl("") + baseUrl, err := url.Parse("https://my.repo.com/v2/chart/tags/list") + if err != nil { + t.Errorf("failed to parse url in test case: %v", err) + } + resp := &http.Response{ + Request: &http.Request{ + URL: baseUrl, + }, + } + nextUrl, err := getNextUrl(resp) assert.Equal(t, nextUrl, "") + assert.NoError(t, err) + + var nextUrlAbsolute = "https://my.repo.com/v2/chart/tags/list?n=123&orderby=" + resp.Header = http.Header{ + "Link": []string{fmt.Sprintf(`<%s>; rel="next"`, nextUrlAbsolute)}, + } + nextUrl, err = getNextUrl(resp) + assert.NoError(t, err) + assert.Equal(t, nextUrl, nextUrlAbsolute) - nextUrl = getNextUrl("; rel=next") - assert.Equal(t, nextUrl, "https://my.repo.com/v2/chart/tags/list?token=123") + var nextUrlRelative = "/v2/chart/tags/list?n=123&orderby=" + resp.Header = http.Header{ + "Link": []string{fmt.Sprintf(`<%s>; rel="next"`, nextUrlRelative)}, + } + nextUrl, err = getNextUrl(resp) + assert.NoError(t, err) + assert.Equal(t, nextUrl, "https://my.repo.com/v2/chart/tags/list?n=123&orderby=") } func Test_getTagsListURL(t *testing.T) { @@ -197,4 +221,14 @@ func Test_getTagsListURL(t *testing.T) { tagsListURL, err = getTagsListURL("https://account.dkr.ecr.eu-central-1.amazonaws.com/", "dss") assert.Nil(t, err) assert.Equal(t, tagsListURL, "https://account.dkr.ecr.eu-central-1.amazonaws.com/v2/dss/tags/list") + + // with unescaped characters allowed by https://www.rfc-editor.org/rfc/rfc3986#page-50 + tagsListURL, err = getTagsListURL("https://account.dkr.ecr.eu-central-1.amazonaws.com/", "charts.-_~$&+=:@dss") + assert.Nil(t, err) + assert.Equal(t, tagsListURL, "https://account.dkr.ecr.eu-central-1.amazonaws.com/v2/charts.-_~$&+=:@dss/tags/list") + + // with escaped characters not allowed in path by https://www.rfc-editor.org/rfc/rfc3986#page-50 + tagsListURL, err = getTagsListURL("https://account.dkr.ecr.eu-central-1.amazonaws.com/", "charts%/dss") + assert.Nil(t, err) + assert.Equal(t, tagsListURL, "https://account.dkr.ecr.eu-central-1.amazonaws.com/v2/charts%25%2Fdss/tags/list") }