github.com/lmars/docker@v1.6.0-rc2/registry/auth.go (about)

     1  package registry
     2  
     3  import (
     4  	"crypto/tls"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	log "github.com/Sirupsen/logrus"
    18  	"github.com/docker/docker/utils"
    19  )
    20  
    21  const (
    22  	// Where we store the config file
    23  	CONFIGFILE = ".dockercfg"
    24  )
    25  
    26  var (
    27  	ErrConfigFileMissing = errors.New("The Auth config file is missing")
    28  )
    29  
    30  type AuthConfig struct {
    31  	Username      string `json:"username,omitempty"`
    32  	Password      string `json:"password,omitempty"`
    33  	Auth          string `json:"auth"`
    34  	Email         string `json:"email"`
    35  	ServerAddress string `json:"serveraddress,omitempty"`
    36  }
    37  
    38  type ConfigFile struct {
    39  	Configs  map[string]AuthConfig `json:"configs,omitempty"`
    40  	rootPath string
    41  }
    42  
    43  type RequestAuthorization struct {
    44  	authConfig       *AuthConfig
    45  	registryEndpoint *Endpoint
    46  	resource         string
    47  	scope            string
    48  	actions          []string
    49  
    50  	tokenLock       sync.Mutex
    51  	tokenCache      string
    52  	tokenExpiration time.Time
    53  }
    54  
    55  func NewRequestAuthorization(authConfig *AuthConfig, registryEndpoint *Endpoint, resource, scope string, actions []string) *RequestAuthorization {
    56  	return &RequestAuthorization{
    57  		authConfig:       authConfig,
    58  		registryEndpoint: registryEndpoint,
    59  		resource:         resource,
    60  		scope:            scope,
    61  		actions:          actions,
    62  	}
    63  }
    64  
    65  func (auth *RequestAuthorization) getToken() (string, error) {
    66  	auth.tokenLock.Lock()
    67  	defer auth.tokenLock.Unlock()
    68  	now := time.Now()
    69  	if now.Before(auth.tokenExpiration) {
    70  		log.Debugf("Using cached token for %s", auth.authConfig.Username)
    71  		return auth.tokenCache, nil
    72  	}
    73  
    74  	tlsConfig := tls.Config{
    75  		MinVersion: tls.VersionTLS10,
    76  	}
    77  	if !auth.registryEndpoint.IsSecure {
    78  		tlsConfig.InsecureSkipVerify = true
    79  	}
    80  
    81  	client := &http.Client{
    82  		Transport: &http.Transport{
    83  			DisableKeepAlives: true,
    84  			Proxy:             http.ProxyFromEnvironment,
    85  			TLSClientConfig:   &tlsConfig,
    86  		},
    87  		CheckRedirect: AddRequiredHeadersToRedirectedRequests,
    88  	}
    89  	factory := HTTPRequestFactory(nil)
    90  
    91  	for _, challenge := range auth.registryEndpoint.AuthChallenges {
    92  		switch strings.ToLower(challenge.Scheme) {
    93  		case "basic":
    94  			// no token necessary
    95  		case "bearer":
    96  			log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, auth.authConfig.Username)
    97  			params := map[string]string{}
    98  			for k, v := range challenge.Parameters {
    99  				params[k] = v
   100  			}
   101  			params["scope"] = fmt.Sprintf("%s:%s:%s", auth.resource, auth.scope, strings.Join(auth.actions, ","))
   102  			token, err := getToken(auth.authConfig.Username, auth.authConfig.Password, params, auth.registryEndpoint, client, factory)
   103  			if err != nil {
   104  				return "", err
   105  			}
   106  			auth.tokenCache = token
   107  			auth.tokenExpiration = now.Add(time.Minute)
   108  
   109  			return token, nil
   110  		default:
   111  			log.Infof("Unsupported auth scheme: %q", challenge.Scheme)
   112  		}
   113  	}
   114  
   115  	// Do not expire cache since there are no challenges which use a token
   116  	auth.tokenExpiration = time.Now().Add(time.Hour * 24)
   117  
   118  	return "", nil
   119  }
   120  
   121  func (auth *RequestAuthorization) Authorize(req *http.Request) error {
   122  	token, err := auth.getToken()
   123  	if err != nil {
   124  		return err
   125  	}
   126  	if token != "" {
   127  		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
   128  	} else if auth.authConfig.Username != "" && auth.authConfig.Password != "" {
   129  		req.SetBasicAuth(auth.authConfig.Username, auth.authConfig.Password)
   130  	}
   131  	return nil
   132  }
   133  
   134  // create a base64 encoded auth string to store in config
   135  func encodeAuth(authConfig *AuthConfig) string {
   136  	authStr := authConfig.Username + ":" + authConfig.Password
   137  	msg := []byte(authStr)
   138  	encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
   139  	base64.StdEncoding.Encode(encoded, msg)
   140  	return string(encoded)
   141  }
   142  
   143  // decode the auth string
   144  func decodeAuth(authStr string) (string, string, error) {
   145  	decLen := base64.StdEncoding.DecodedLen(len(authStr))
   146  	decoded := make([]byte, decLen)
   147  	authByte := []byte(authStr)
   148  	n, err := base64.StdEncoding.Decode(decoded, authByte)
   149  	if err != nil {
   150  		return "", "", err
   151  	}
   152  	if n > decLen {
   153  		return "", "", fmt.Errorf("Something went wrong decoding auth config")
   154  	}
   155  	arr := strings.SplitN(string(decoded), ":", 2)
   156  	if len(arr) != 2 {
   157  		return "", "", fmt.Errorf("Invalid auth configuration file")
   158  	}
   159  	password := strings.Trim(arr[1], "\x00")
   160  	return arr[0], password, nil
   161  }
   162  
   163  // load up the auth config information and return values
   164  // FIXME: use the internal golang config parser
   165  func LoadConfig(rootPath string) (*ConfigFile, error) {
   166  	configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
   167  	confFile := path.Join(rootPath, CONFIGFILE)
   168  	if _, err := os.Stat(confFile); err != nil {
   169  		return &configFile, nil //missing file is not an error
   170  	}
   171  	b, err := ioutil.ReadFile(confFile)
   172  	if err != nil {
   173  		return &configFile, err
   174  	}
   175  
   176  	if err := json.Unmarshal(b, &configFile.Configs); err != nil {
   177  		arr := strings.Split(string(b), "\n")
   178  		if len(arr) < 2 {
   179  			return &configFile, fmt.Errorf("The Auth config file is empty")
   180  		}
   181  		authConfig := AuthConfig{}
   182  		origAuth := strings.Split(arr[0], " = ")
   183  		if len(origAuth) != 2 {
   184  			return &configFile, fmt.Errorf("Invalid Auth config file")
   185  		}
   186  		authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1])
   187  		if err != nil {
   188  			return &configFile, err
   189  		}
   190  		origEmail := strings.Split(arr[1], " = ")
   191  		if len(origEmail) != 2 {
   192  			return &configFile, fmt.Errorf("Invalid Auth config file")
   193  		}
   194  		authConfig.Email = origEmail[1]
   195  		authConfig.ServerAddress = IndexServerAddress()
   196  		// *TODO: Switch to using IndexServerName() instead?
   197  		configFile.Configs[IndexServerAddress()] = authConfig
   198  	} else {
   199  		for k, authConfig := range configFile.Configs {
   200  			authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
   201  			if err != nil {
   202  				return &configFile, err
   203  			}
   204  			authConfig.Auth = ""
   205  			authConfig.ServerAddress = k
   206  			configFile.Configs[k] = authConfig
   207  		}
   208  	}
   209  	return &configFile, nil
   210  }
   211  
   212  // save the auth config
   213  func SaveConfig(configFile *ConfigFile) error {
   214  	confFile := path.Join(configFile.rootPath, CONFIGFILE)
   215  	if len(configFile.Configs) == 0 {
   216  		os.Remove(confFile)
   217  		return nil
   218  	}
   219  
   220  	configs := make(map[string]AuthConfig, len(configFile.Configs))
   221  	for k, authConfig := range configFile.Configs {
   222  		authCopy := authConfig
   223  
   224  		authCopy.Auth = encodeAuth(&authCopy)
   225  		authCopy.Username = ""
   226  		authCopy.Password = ""
   227  		authCopy.ServerAddress = ""
   228  		configs[k] = authCopy
   229  	}
   230  
   231  	b, err := json.MarshalIndent(configs, "", "\t")
   232  	if err != nil {
   233  		return err
   234  	}
   235  	err = ioutil.WriteFile(confFile, b, 0600)
   236  	if err != nil {
   237  		return err
   238  	}
   239  	return nil
   240  }
   241  
   242  // Login tries to register/login to the registry server.
   243  func Login(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) {
   244  	// Separates the v2 registry login logic from the v1 logic.
   245  	if registryEndpoint.Version == APIVersion2 {
   246  		return loginV2(authConfig, registryEndpoint, factory)
   247  	}
   248  
   249  	return loginV1(authConfig, registryEndpoint, factory)
   250  }
   251  
   252  // loginV1 tries to register/login to the v1 registry server.
   253  func loginV1(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) {
   254  	var (
   255  		status  string
   256  		reqBody []byte
   257  		err     error
   258  		client  = &http.Client{
   259  			Transport: &http.Transport{
   260  				DisableKeepAlives: true,
   261  				Proxy:             http.ProxyFromEnvironment,
   262  			},
   263  			CheckRedirect: AddRequiredHeadersToRedirectedRequests,
   264  		}
   265  		reqStatusCode = 0
   266  		serverAddress = authConfig.ServerAddress
   267  	)
   268  
   269  	log.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
   270  
   271  	if serverAddress == "" {
   272  		return "", fmt.Errorf("Server Error: Server Address not set.")
   273  	}
   274  
   275  	loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
   276  
   277  	// to avoid sending the server address to the server it should be removed before being marshalled
   278  	authCopy := *authConfig
   279  	authCopy.ServerAddress = ""
   280  
   281  	jsonBody, err := json.Marshal(authCopy)
   282  	if err != nil {
   283  		return "", fmt.Errorf("Config Error: %s", err)
   284  	}
   285  
   286  	// using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
   287  	b := strings.NewReader(string(jsonBody))
   288  	req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b)
   289  	if err != nil {
   290  		return "", fmt.Errorf("Server Error: %s", err)
   291  	}
   292  	reqStatusCode = req1.StatusCode
   293  	defer req1.Body.Close()
   294  	reqBody, err = ioutil.ReadAll(req1.Body)
   295  	if err != nil {
   296  		return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err)
   297  	}
   298  
   299  	if reqStatusCode == 201 {
   300  		if loginAgainstOfficialIndex {
   301  			status = "Account created. Please use the confirmation link we sent" +
   302  				" to your e-mail to activate it."
   303  		} else {
   304  			// *TODO: Use registry configuration to determine what this says, if anything?
   305  			status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
   306  		}
   307  	} else if reqStatusCode == 400 {
   308  		if string(reqBody) == "\"Username or email already exists\"" {
   309  			req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
   310  			req.SetBasicAuth(authConfig.Username, authConfig.Password)
   311  			resp, err := client.Do(req)
   312  			if err != nil {
   313  				return "", err
   314  			}
   315  			defer resp.Body.Close()
   316  			body, err := ioutil.ReadAll(resp.Body)
   317  			if err != nil {
   318  				return "", err
   319  			}
   320  			if resp.StatusCode == 200 {
   321  				return "Login Succeeded", nil
   322  			} else if resp.StatusCode == 401 {
   323  				return "", fmt.Errorf("Wrong login/password, please try again")
   324  			} else if resp.StatusCode == 403 {
   325  				if loginAgainstOfficialIndex {
   326  					return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.")
   327  				}
   328  				// *TODO: Use registry configuration to determine what this says, if anything?
   329  				return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
   330  			}
   331  			return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header)
   332  		}
   333  		return "", fmt.Errorf("Registration: %s", reqBody)
   334  
   335  	} else if reqStatusCode == 401 {
   336  		// This case would happen with private registries where /v1/users is
   337  		// protected, so people can use `docker login` as an auth check.
   338  		req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
   339  		req.SetBasicAuth(authConfig.Username, authConfig.Password)
   340  		resp, err := client.Do(req)
   341  		if err != nil {
   342  			return "", err
   343  		}
   344  		defer resp.Body.Close()
   345  		body, err := ioutil.ReadAll(resp.Body)
   346  		if err != nil {
   347  			return "", err
   348  		}
   349  		if resp.StatusCode == 200 {
   350  			return "Login Succeeded", nil
   351  		} else if resp.StatusCode == 401 {
   352  			return "", fmt.Errorf("Wrong login/password, please try again")
   353  		} else {
   354  			return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
   355  				resp.StatusCode, resp.Header)
   356  		}
   357  	} else {
   358  		return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
   359  	}
   360  	return status, nil
   361  }
   362  
   363  // loginV2 tries to login to the v2 registry server. The given registry endpoint has been
   364  // pinged or setup with a list of authorization challenges. Each of these challenges are
   365  // tried until one of them succeeds. Currently supported challenge schemes are:
   366  // 		HTTP Basic Authorization
   367  // 		Token Authorization with a separate token issuing server
   368  // NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For
   369  // now, users should create their account through other means like directly from a web page
   370  // served by the v2 registry service provider. Whether this will be supported in the future
   371  // is to be determined.
   372  func loginV2(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) {
   373  	log.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint)
   374  
   375  	tlsConfig := tls.Config{
   376  		MinVersion: tls.VersionTLS10,
   377  	}
   378  	if !registryEndpoint.IsSecure {
   379  		tlsConfig.InsecureSkipVerify = true
   380  	}
   381  
   382  	client := &http.Client{
   383  		Transport: &http.Transport{
   384  			DisableKeepAlives: true,
   385  			Proxy:             http.ProxyFromEnvironment,
   386  			TLSClientConfig:   &tlsConfig,
   387  		},
   388  		CheckRedirect: AddRequiredHeadersToRedirectedRequests,
   389  	}
   390  
   391  	var (
   392  		err       error
   393  		allErrors []error
   394  	)
   395  
   396  	for _, challenge := range registryEndpoint.AuthChallenges {
   397  		log.Debugf("trying %q auth challenge with params %s", challenge.Scheme, challenge.Parameters)
   398  
   399  		switch strings.ToLower(challenge.Scheme) {
   400  		case "basic":
   401  			err = tryV2BasicAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory)
   402  		case "bearer":
   403  			err = tryV2TokenAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory)
   404  		default:
   405  			// Unsupported challenge types are explicitly skipped.
   406  			err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme)
   407  		}
   408  
   409  		if err == nil {
   410  			return "Login Succeeded", nil
   411  		}
   412  
   413  		log.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err)
   414  
   415  		allErrors = append(allErrors, err)
   416  	}
   417  
   418  	return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors)
   419  }
   420  
   421  func tryV2BasicAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error {
   422  	req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil)
   423  	if err != nil {
   424  		return err
   425  	}
   426  
   427  	req.SetBasicAuth(authConfig.Username, authConfig.Password)
   428  
   429  	resp, err := client.Do(req)
   430  	if err != nil {
   431  		return err
   432  	}
   433  	defer resp.Body.Close()
   434  
   435  	if resp.StatusCode != http.StatusOK {
   436  		return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
   437  	}
   438  
   439  	return nil
   440  }
   441  
   442  func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error {
   443  	token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint, client, factory)
   444  	if err != nil {
   445  		return err
   446  	}
   447  
   448  	req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil)
   449  	if err != nil {
   450  		return err
   451  	}
   452  
   453  	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
   454  
   455  	resp, err := client.Do(req)
   456  	if err != nil {
   457  		return err
   458  	}
   459  	defer resp.Body.Close()
   460  
   461  	if resp.StatusCode != http.StatusOK {
   462  		return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
   463  	}
   464  
   465  	return nil
   466  }
   467  
   468  // this method matches a auth configuration to a server address or a url
   469  func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
   470  	configKey := index.GetAuthConfigKey()
   471  	// First try the happy case
   472  	if c, found := config.Configs[configKey]; found || index.Official {
   473  		return c
   474  	}
   475  
   476  	convertToHostname := func(url string) string {
   477  		stripped := url
   478  		if strings.HasPrefix(url, "http://") {
   479  			stripped = strings.Replace(url, "http://", "", 1)
   480  		} else if strings.HasPrefix(url, "https://") {
   481  			stripped = strings.Replace(url, "https://", "", 1)
   482  		}
   483  
   484  		nameParts := strings.SplitN(stripped, "/", 2)
   485  
   486  		return nameParts[0]
   487  	}
   488  
   489  	// Maybe they have a legacy config file, we will iterate the keys converting
   490  	// them to the new format and testing
   491  	for registry, config := range config.Configs {
   492  		if configKey == convertToHostname(registry) {
   493  			return config
   494  		}
   495  	}
   496  
   497  	// When all else fails, return an empty auth config
   498  	return AuthConfig{}
   499  }