-
Notifications
You must be signed in to change notification settings - Fork 812
bugfix : fix image auth bug #2900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ import ( | |
| "io" | ||
| "io/fs" | ||
| "net/http" | ||
| "net/url" | ||
| "os" | ||
| "path/filepath" | ||
| "regexp" | ||
|
|
@@ -177,16 +178,17 @@ func (i imagePullArgs) pull(ctx context.Context, platform string) error { | |
| if err != nil { | ||
| return errors.Wrapf(err, "failed to get remote image %s", img) | ||
| } | ||
| selectedAuth := selectAuth(img, i.auths) | ||
| src.Client = &auth.Client{ | ||
| Client: &http.Client{ | ||
| Transport: &http.Transport{ | ||
| TLSClientConfig: &tls.Config{ | ||
| InsecureSkipVerify: skipTLSVerifyFunc(img, i.auths, *i.skipTLSVerify), | ||
| InsecureSkipVerify: skipTLSVerifyFunc(selectedAuth, *i.skipTLSVerify), | ||
| }, | ||
| }, | ||
| }, | ||
| Cache: auth.NewCache(), | ||
| Credential: authFunc(i.auths), | ||
| Credential: authFunc(selectedAuth), | ||
| } | ||
|
|
||
| dst, err := newLocalRepository(filepath.Join(src.Reference.Registry, src.Reference.Repository)+":"+src.Reference.Reference, i.imagesDir) | ||
|
|
@@ -204,7 +206,7 @@ func (i imagePullArgs) pull(ctx context.Context, platform string) error { | |
| } | ||
| } | ||
|
|
||
| src.PlainHTTP = plainHTTPFunc(img, i.auths, false) | ||
| src.PlainHTTP = plainHTTPFunc(selectedAuth, false) | ||
|
|
||
| if _, err = oras.Copy(ctx, src, src.Reference.Reference, dst, "", copyOption); err != nil { | ||
| return errors.Wrapf(err, "failed to pull image %q to local dir", img) | ||
|
|
@@ -228,50 +230,187 @@ func dockerHostParser(img string) string { | |
| return strings.Join(splitedImg, "/") | ||
| } | ||
|
|
||
| func authFunc(auths []imageAuth) func(ctx context.Context, hostport string) (auth.Credential, error) { | ||
| var creds = make(map[string]auth.Credential) | ||
| for _, inputAuth := range auths { | ||
| var rp = inputAuth.Repo | ||
| if rp == "docker.io" || rp == "" { | ||
| rp = "registry-1.docker.io" | ||
| func authFunc(selectedAuth *imageAuth) func(ctx context.Context, hostport string) (auth.Credential, error) { | ||
| return func(_ context.Context, _ string) (auth.Credential, error) { | ||
| if selectedAuth == nil { | ||
| return auth.Credential{ | ||
| Username: "", | ||
| Password: "", | ||
| }, nil | ||
| } | ||
| return auth.Credential{ | ||
| Username: selectedAuth.Username, | ||
| Password: selectedAuth.Password, | ||
| }, nil | ||
| } | ||
| } | ||
|
|
||
| func selectAuth(image string, authList []imageAuth) *imageAuth { | ||
| // remove tag and hash | ||
| repoPart := extractRepoFromImage(image) | ||
|
|
||
| // parse image to url | ||
| imageURL, err := normalizeImageToURL(repoPart) | ||
| if err != nil { | ||
| return nil | ||
| } | ||
|
|
||
| imageHost := imageURL.Host | ||
| imagePath := strings.TrimPrefix(imageURL.Path, "/") | ||
|
|
||
| // split port | ||
| imageHostWithoutPort := imageHost | ||
| if colonIdx := strings.Index(imageHost, ":"); colonIdx != -1 { | ||
| imageHostWithoutPort = imageHost[:colonIdx] | ||
| } | ||
|
|
||
| var bestMatch *imageAuth | ||
| var bestMatchScore = -1 | ||
|
|
||
| // find biggest match point auth | ||
| for i := range authList { | ||
| authRepo := authList[i].Repo | ||
|
|
||
| authURL, err := normalizeImageToURL(authRepo) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| creds[rp] = auth.Credential{ | ||
| Username: inputAuth.Username, | ||
| Password: inputAuth.Password, | ||
|
|
||
| authHost := authURL.Host | ||
| authPath := strings.TrimPrefix(authURL.Path, "/") | ||
|
|
||
| authHostWithoutPort := authHost | ||
| if colonIdx := strings.Index(authHost, ":"); colonIdx != -1 { | ||
| authHostWithoutPort = authHost[:colonIdx] | ||
| } | ||
|
|
||
| score := calculateMatchScore(imageHost, imageHostWithoutPort, imagePath, | ||
| authHost, authHostWithoutPort, authPath) | ||
|
|
||
| if score > bestMatchScore { | ||
| bestMatchScore = score | ||
| bestMatch = &authList[i] | ||
| } | ||
| } | ||
| return func(_ context.Context, hostport string) (auth.Credential, error) { | ||
| cred, ok := creds[hostport] | ||
| if !ok { | ||
| cred = auth.EmptyCredential | ||
|
|
||
| return bestMatch | ||
| } | ||
|
|
||
| func normalizeImageToURL(image string) (*url.URL, error) { | ||
| if strings.HasPrefix(image, "http://") || strings.HasPrefix(image, "https://") { | ||
| return url.Parse(image) | ||
| } | ||
| return url.Parse("http://" + image) | ||
| } | ||
|
|
||
| // extractRepoFromImage handle image ,ignore tag and hash | ||
| // like xxx/xxx:tag@sha256:xxx to xxx/xxx | ||
| // like xxx/xxx:tag to xxx/xxx | ||
| func extractRepoFromImage(image string) string { | ||
| if image == "" { | ||
| return "" | ||
| } | ||
|
|
||
| repoPart := image | ||
|
|
||
| // handle image with hash | ||
| atIdx := strings.LastIndex(repoPart, "@") | ||
| if atIdx != -1 && atIdx > 0 { | ||
| afterAt := repoPart[atIdx+1:] | ||
| if isLikelyDigest(afterAt) { | ||
| repoPart = repoPart[:atIdx] | ||
| } | ||
| } | ||
|
|
||
| // search / | ||
| firstSlashIdx := strings.Index(repoPart, "/") | ||
|
|
||
| if firstSlashIdx == -1 { | ||
| return repoPart | ||
| } | ||
|
|
||
| // search : for tag or port | ||
| lastColonIdx := strings.LastIndex(repoPart, ":") | ||
| if lastColonIdx == -1 { | ||
| return repoPart | ||
| } | ||
|
|
||
| if lastColonIdx < firstSlashIdx { | ||
| return repoPart | ||
| } | ||
|
|
||
| if lastColonIdx+1 < len(repoPart) { | ||
| afterColon := repoPart[lastColonIdx+1:] | ||
| if strings.Contains(afterColon, "/") { | ||
| return repoPart | ||
| } | ||
| return cred, nil | ||
| } | ||
| return repoPart[:lastColonIdx] | ||
| } | ||
|
|
||
| func skipTLSVerifyFunc(img string, auths []imageAuth, defaults bool) bool { | ||
| imgHost := strings.Split(img, "/")[0] | ||
| for _, a := range auths { | ||
| if imgHost == a.Repo { | ||
| if a.Insecure != nil { | ||
| return *a.Insecure | ||
| } | ||
| return defaults | ||
| func isLikelyDigest(s string) bool { | ||
| colonIdx := strings.Index(s, ":") | ||
| if colonIdx == -1 { | ||
| return false | ||
| } | ||
| algorithm := s[:colonIdx] | ||
| knownAlgorithms := []string{"sha256", "sha512", "sha384", "sha1", "md5"} | ||
| for _, algo := range knownAlgorithms { | ||
| if algorithm == algo { | ||
| return true | ||
| } | ||
| } | ||
| return defaults | ||
| return false | ||
| } | ||
|
|
||
| func plainHTTPFunc(img string, auths []imageAuth, defaults bool) bool { | ||
| imgHost := strings.Split(img, "/")[0] | ||
| for _, a := range auths { | ||
| if imgHost == a.Repo { | ||
| if a.PlainHTTP != nil { | ||
| return *a.PlainHTTP | ||
| } | ||
| return defaults | ||
| // calculateMatchScore calculate image and path match point | ||
| // 0 for not matched | ||
| // 1 for host matched | ||
| // 2 for host:port matched | ||
| // 3 for host:port and prefix path matched | ||
| // 4 for host:port and full path matched | ||
| func calculateMatchScore(imgHost, imgHostNoPort, imgPath, | ||
| authHost, authHostNoPort, authPath string) int { | ||
| if imgHostNoPort != authHostNoPort { | ||
| return 0 | ||
| } | ||
| score := 1 | ||
| imgHasPort := strings.Contains(imgHost, ":") | ||
| authHasPort := strings.Contains(authHost, ":") | ||
| if imgHasPort && authHasPort { | ||
| if imgHost != authHost { | ||
| return 1 | ||
| } | ||
| score = 2 | ||
| } else if !imgHasPort && !authHasPort { | ||
| score = 2 | ||
| } | ||
| if authPath == "" { | ||
| return score | ||
| } | ||
| if imgPath == authPath { | ||
| return score + 2 | ||
| } | ||
| if strings.HasPrefix(imgPath, authPath) { | ||
| if len(imgPath) == len(authPath) || | ||
| (len(imgPath) > len(authPath) && imgPath[len(authPath)] == '/') { | ||
| return score + 1 | ||
| } | ||
| } | ||
| return 0 | ||
| } | ||
|
|
||
| func skipTLSVerifyFunc(selectedAuth *imageAuth, defaults bool) bool { | ||
| if selectedAuth != nil && selectedAuth.Insecure != nil { | ||
| return *selectedAuth.Insecure | ||
| } | ||
| return defaults | ||
| } | ||
|
|
||
| func plainHTTPFunc(selectedAuth *imageAuth, defaults bool) bool { | ||
| if selectedAuth != nil && selectedAuth.PlainHTTP != nil { | ||
| return *selectedAuth.PlainHTTP | ||
| } | ||
| return defaults | ||
| } | ||
|
|
||
|
|
@@ -337,19 +476,20 @@ func (i imagePushArgs) push(ctx context.Context, hostVars map[string]any) error | |
| if err != nil { | ||
| return errors.Wrapf(err, "failed to get remote repository %q", dest) | ||
| } | ||
| selectedAuth := selectAuth(dest, i.auths) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the |
||
| dst.Client = &auth.Client{ | ||
| Client: &http.Client{ | ||
| Transport: &http.Transport{ | ||
| TLSClientConfig: &tls.Config{ | ||
| InsecureSkipVerify: skipTLSVerifyFunc(dest, i.auths, *i.skipTLSVerify), | ||
| InsecureSkipVerify: skipTLSVerifyFunc(selectedAuth, *i.skipTLSVerify), | ||
| }, | ||
| }, | ||
| }, | ||
| Cache: auth.NewCache(), | ||
| Credential: authFunc(i.auths), | ||
| Credential: authFunc(selectedAuth), | ||
| } | ||
|
|
||
| dst.PlainHTTP = plainHTTPFunc(dest, i.auths, false) | ||
| dst.PlainHTTP = plainHTTPFunc(selectedAuth, false) | ||
|
|
||
| if _, err = oras.Copy(ctx, src, src.Reference.Reference, dst, dst.Reference.Reference, oras.DefaultCopyOptions); err != nil { | ||
| return errors.Wrapf(err, "failed to push image %q to remote", img) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While
CredentialandInsecureSkipVerifyare now correctly configured usingselectedAuth, theplainHTTPsetting is determined later on line 208 usingplainHTTPFunc(img, i.auths, false), which still contains the old logic that doesn't respect longest-prefix matching. For consistency and correctness,plainHTTPFuncshould be refactored to acceptselectedAuth(similar to the change made forskipTLSVerifyFunc), and then used to setsrc.PlainHTTP.