github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/login/login.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package login 18 19 import ( 20 "bufio" 21 "context" 22 "errors" 23 "fmt" 24 "io" 25 "net/http" 26 "net/url" 27 "os" 28 "strings" 29 30 "github.com/containerd/containerd/remotes/docker" 31 "github.com/containerd/containerd/remotes/docker/config" 32 "github.com/containerd/log" 33 "github.com/containerd/nerdctl/v2/pkg/api/types" 34 "github.com/containerd/nerdctl/v2/pkg/errutil" 35 "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" 36 dockercliconfig "github.com/docker/cli/cli/config" 37 dockercliconfigtypes "github.com/docker/cli/cli/config/types" 38 "github.com/docker/docker/api/types/registry" 39 "github.com/docker/docker/errdefs" 40 "golang.org/x/net/context/ctxhttp" 41 "golang.org/x/term" 42 ) 43 44 const unencryptedPasswordWarning = `WARNING: Your password will be stored unencrypted in %s. 45 Configure a credential helper to remove this warning. See 46 https://docs.docker.com/engine/reference/commandline/login/#credentials-store 47 ` 48 49 type isFileStore interface { 50 IsFileStore() bool 51 GetFilename() string 52 } 53 54 func Login(ctx context.Context, options types.LoginCommandOptions, stdout io.Writer) error { 55 var serverAddress string 56 if options.ServerAddress == "" { 57 serverAddress = dockerconfigresolver.IndexServer 58 } else { 59 serverAddress = options.ServerAddress 60 } 61 62 var responseIdentityToken string 63 isDefaultRegistry := serverAddress == dockerconfigresolver.IndexServer 64 65 authConfig, err := GetDefaultAuthConfig(options.Username == "" && options.Password == "", serverAddress, isDefaultRegistry) 66 if authConfig == nil { 67 authConfig = ®istry.AuthConfig{ServerAddress: serverAddress} 68 } 69 if err == nil && authConfig.Username != "" && authConfig.Password != "" { 70 //login With StoreCreds 71 responseIdentityToken, err = loginClientSide(ctx, options.GOptions, *authConfig) 72 } 73 74 if err != nil || authConfig.Username == "" || authConfig.Password == "" { 75 err = ConfigureAuthentication(authConfig, options.Username, options.Password) 76 if err != nil { 77 return err 78 } 79 80 responseIdentityToken, err = loginClientSide(ctx, options.GOptions, *authConfig) 81 if err != nil { 82 return err 83 } 84 } 85 86 if responseIdentityToken != "" { 87 authConfig.Password = "" 88 authConfig.IdentityToken = responseIdentityToken 89 } 90 91 dockerConfigFile, err := dockercliconfig.Load("") 92 if err != nil { 93 return err 94 } 95 96 creds := dockerConfigFile.GetCredentialsStore(serverAddress) 97 98 store, isFile := creds.(isFileStore) 99 // Display a warning if we're storing the users password (not a token) and credentials store type is file. 100 if isFile && authConfig.Password != "" { 101 _, err = fmt.Fprintln(stdout, fmt.Sprintf(unencryptedPasswordWarning, store.GetFilename())) 102 if err != nil { 103 return err 104 } 105 } 106 107 if err := creds.Store(dockercliconfigtypes.AuthConfig(*(authConfig))); err != nil { 108 return fmt.Errorf("error saving credentials: %w", err) 109 } 110 111 fmt.Fprintln(stdout, "Login Succeeded") 112 113 return nil 114 } 115 116 // GetDefaultAuthConfig gets the default auth config given a serverAddress. 117 // If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it. 118 // Code from github.com/docker/cli/cli/command (v20.10.3). 119 func GetDefaultAuthConfig(checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*registry.AuthConfig, error) { 120 if !isDefaultRegistry { 121 var err error 122 serverAddress, err = convertToHostname(serverAddress) 123 if err != nil { 124 return nil, err 125 } 126 } 127 var authconfig = dockercliconfigtypes.AuthConfig{} 128 if checkCredStore { 129 dockerConfigFile, err := dockercliconfig.Load("") 130 if err != nil { 131 return nil, err 132 } 133 authconfig, err = dockerConfigFile.GetAuthConfig(serverAddress) 134 if err != nil { 135 return nil, err 136 } 137 } 138 authconfig.ServerAddress = serverAddress 139 authconfig.IdentityToken = "" 140 res := registry.AuthConfig(authconfig) 141 return &res, nil 142 } 143 144 func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptions, auth registry.AuthConfig) (string, error) { 145 host, err := convertToHostname(auth.ServerAddress) 146 if err != nil { 147 return "", err 148 } 149 var dOpts []dockerconfigresolver.Opt 150 if globalOptions.InsecureRegistry { 151 log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", host) 152 dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) 153 } 154 dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(globalOptions.HostsDir)) 155 156 authCreds := func(acArg string) (string, string, error) { 157 if acArg == host { 158 if auth.RegistryToken != "" { 159 // Even containerd/CRI does not support RegistryToken as of v1.4.3, 160 // so, nobody is actually using RegistryToken? 161 log.G(ctx).Warnf("RegistryToken (for %q) is not supported yet (FIXME)", host) 162 } 163 return auth.Username, auth.Password, nil 164 } 165 return "", "", fmt.Errorf("expected acArg to be %q, got %q", host, acArg) 166 } 167 168 dOpts = append(dOpts, dockerconfigresolver.WithAuthCreds(authCreds)) 169 ho, err := dockerconfigresolver.NewHostOptions(ctx, host, dOpts...) 170 if err != nil { 171 return "", err 172 } 173 fetchedRefreshTokens := make(map[string]string) // key: req.URL.Host 174 // onFetchRefreshToken is called when tryLoginWithRegHost calls rh.Authorizer.Authorize() 175 onFetchRefreshToken := func(ctx context.Context, s string, req *http.Request) { 176 fetchedRefreshTokens[req.URL.Host] = s 177 } 178 ho.AuthorizerOpts = append(ho.AuthorizerOpts, docker.WithFetchRefreshToken(onFetchRefreshToken)) 179 regHosts, err := config.ConfigureHosts(ctx, *ho)(host) 180 if err != nil { 181 return "", err 182 } 183 log.G(ctx).Debugf("len(regHosts)=%d", len(regHosts)) 184 if len(regHosts) == 0 { 185 return "", fmt.Errorf("got empty []docker.RegistryHost for %q", host) 186 } 187 for i, rh := range regHosts { 188 err = tryLoginWithRegHost(ctx, rh) 189 if err != nil && globalOptions.InsecureRegistry && (errutil.IsErrHTTPResponseToHTTPSClient(err) || errutil.IsErrConnectionRefused(err)) { 190 rh.Scheme = "http" 191 err = tryLoginWithRegHost(ctx, rh) 192 } 193 identityToken := fetchedRefreshTokens[rh.Host] // can be empty 194 if err == nil { 195 return identityToken, nil 196 } 197 log.G(ctx).WithError(err).WithField("i", i).Error("failed to call tryLoginWithRegHost") 198 } 199 return "", err 200 } 201 202 func tryLoginWithRegHost(ctx context.Context, rh docker.RegistryHost) error { 203 if rh.Authorizer == nil { 204 return errors.New("got nil Authorizer") 205 } 206 if rh.Path == "/v2" { 207 // If the path is using /v2 endpoint but lacks trailing slash add it 208 // https://docs.docker.com/registry/spec/api/#detail. Acts as a workaround 209 // for containerd issue https://github.com/containerd/containerd/blob/2986d5b077feb8252d5d2060277a9c98ff8e009b/remotes/docker/config/hosts.go#L110 210 rh.Path = "/v2/" 211 } 212 u := url.URL{ 213 Scheme: rh.Scheme, 214 Host: rh.Host, 215 Path: rh.Path, 216 } 217 var ress []*http.Response 218 for i := 0; i < 10; i++ { 219 req, err := http.NewRequest(http.MethodGet, u.String(), nil) 220 if err != nil { 221 return err 222 } 223 for k, v := range rh.Header.Clone() { 224 for _, vv := range v { 225 req.Header.Add(k, vv) 226 } 227 } 228 if err := rh.Authorizer.Authorize(ctx, req); err != nil { 229 return fmt.Errorf("failed to call rh.Authorizer.Authorize: %w", err) 230 } 231 res, err := ctxhttp.Do(ctx, rh.Client, req) 232 if err != nil { 233 return fmt.Errorf("failed to call rh.Client.Do: %w", err) 234 } 235 ress = append(ress, res) 236 if res.StatusCode == 401 { 237 if err := rh.Authorizer.AddResponses(ctx, ress); err != nil && !errdefs.IsNotImplemented(err) { 238 return fmt.Errorf("failed to call rh.Authorizer.AddResponses: %w", err) 239 } 240 continue 241 } 242 if res.StatusCode/100 != 2 { 243 return fmt.Errorf("unexpected status code %d", res.StatusCode) 244 } 245 246 return nil 247 } 248 249 return errors.New("too many 401 (probably)") 250 } 251 252 func ConfigureAuthentication(authConfig *registry.AuthConfig, username, password string) error { 253 authConfig.Username = strings.TrimSpace(authConfig.Username) 254 if username = strings.TrimSpace(username); username == "" { 255 username = authConfig.Username 256 } 257 if username == "" { 258 fmt.Print("Enter Username: ") 259 usr, err := readUsername() 260 if err != nil { 261 return err 262 } 263 username = usr 264 } 265 if username == "" { 266 return fmt.Errorf("error: Username is Required") 267 } 268 269 if password == "" { 270 fmt.Print("Enter Password: ") 271 pwd, err := readPassword() 272 fmt.Println() 273 if err != nil { 274 return err 275 } 276 password = pwd 277 } 278 if password == "" { 279 return fmt.Errorf("error: Password is Required") 280 } 281 282 authConfig.Username = username 283 authConfig.Password = password 284 285 return nil 286 } 287 288 func readUsername() (string, error) { 289 var fd *os.File 290 if term.IsTerminal(int(os.Stdin.Fd())) { 291 fd = os.Stdin 292 } else { 293 return "", fmt.Errorf("stdin is not a terminal (Hint: use `nerdctl login --username=USERNAME --password-stdin`)") 294 } 295 296 reader := bufio.NewReader(fd) 297 username, err := reader.ReadString('\n') 298 if err != nil { 299 return "", fmt.Errorf("error reading username: %w", err) 300 } 301 username = strings.TrimSpace(username) 302 303 return username, nil 304 } 305 306 func convertToHostname(serverAddress string) (string, error) { 307 // Ensure that URL contains scheme for a good parsing process 308 if strings.Contains(serverAddress, "://") { 309 u, err := url.Parse(serverAddress) 310 if err != nil { 311 return "", err 312 } 313 serverAddress = u.Host 314 } else { 315 u, err := url.Parse("https://" + serverAddress) 316 if err != nil { 317 return "", err 318 } 319 serverAddress = u.Host 320 } 321 322 return serverAddress, nil 323 }