github.com/titanous/docker@v1.4.1/registry/auth.go (about)

     1  package registry
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  
    15  	"github.com/docker/docker/utils"
    16  )
    17  
    18  const (
    19  	// Where we store the config file
    20  	CONFIGFILE = ".dockercfg"
    21  
    22  	// Only used for user auth + account creation
    23  	INDEXSERVER    = "https://index.docker.io/v1/"
    24  	REGISTRYSERVER = "https://registry-1.docker.io/v1/"
    25  
    26  	// INDEXSERVER = "https://registry-stage.hub.docker.com/v1/"
    27  )
    28  
    29  var (
    30  	ErrConfigFileMissing = errors.New("The Auth config file is missing")
    31  	IndexServerURL       *url.URL
    32  )
    33  
    34  func init() {
    35  	url, err := url.Parse(INDEXSERVER)
    36  	if err != nil {
    37  		panic(err)
    38  	}
    39  	IndexServerURL = url
    40  }
    41  
    42  type AuthConfig struct {
    43  	Username      string `json:"username,omitempty"`
    44  	Password      string `json:"password,omitempty"`
    45  	Auth          string `json:"auth"`
    46  	Email         string `json:"email"`
    47  	ServerAddress string `json:"serveraddress,omitempty"`
    48  }
    49  
    50  type ConfigFile struct {
    51  	Configs  map[string]AuthConfig `json:"configs,omitempty"`
    52  	rootPath string
    53  }
    54  
    55  func IndexServerAddress() string {
    56  	return INDEXSERVER
    57  }
    58  
    59  // create a base64 encoded auth string to store in config
    60  func encodeAuth(authConfig *AuthConfig) string {
    61  	authStr := authConfig.Username + ":" + authConfig.Password
    62  	msg := []byte(authStr)
    63  	encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
    64  	base64.StdEncoding.Encode(encoded, msg)
    65  	return string(encoded)
    66  }
    67  
    68  // decode the auth string
    69  func decodeAuth(authStr string) (string, string, error) {
    70  	decLen := base64.StdEncoding.DecodedLen(len(authStr))
    71  	decoded := make([]byte, decLen)
    72  	authByte := []byte(authStr)
    73  	n, err := base64.StdEncoding.Decode(decoded, authByte)
    74  	if err != nil {
    75  		return "", "", err
    76  	}
    77  	if n > decLen {
    78  		return "", "", fmt.Errorf("Something went wrong decoding auth config")
    79  	}
    80  	arr := strings.SplitN(string(decoded), ":", 2)
    81  	if len(arr) != 2 {
    82  		return "", "", fmt.Errorf("Invalid auth configuration file")
    83  	}
    84  	password := strings.Trim(arr[1], "\x00")
    85  	return arr[0], password, nil
    86  }
    87  
    88  // load up the auth config information and return values
    89  // FIXME: use the internal golang config parser
    90  func LoadConfig(rootPath string) (*ConfigFile, error) {
    91  	configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
    92  	confFile := path.Join(rootPath, CONFIGFILE)
    93  	if _, err := os.Stat(confFile); err != nil {
    94  		return &configFile, nil //missing file is not an error
    95  	}
    96  	b, err := ioutil.ReadFile(confFile)
    97  	if err != nil {
    98  		return &configFile, err
    99  	}
   100  
   101  	if err := json.Unmarshal(b, &configFile.Configs); err != nil {
   102  		arr := strings.Split(string(b), "\n")
   103  		if len(arr) < 2 {
   104  			return &configFile, fmt.Errorf("The Auth config file is empty")
   105  		}
   106  		authConfig := AuthConfig{}
   107  		origAuth := strings.Split(arr[0], " = ")
   108  		if len(origAuth) != 2 {
   109  			return &configFile, fmt.Errorf("Invalid Auth config file")
   110  		}
   111  		authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1])
   112  		if err != nil {
   113  			return &configFile, err
   114  		}
   115  		origEmail := strings.Split(arr[1], " = ")
   116  		if len(origEmail) != 2 {
   117  			return &configFile, fmt.Errorf("Invalid Auth config file")
   118  		}
   119  		authConfig.Email = origEmail[1]
   120  		authConfig.ServerAddress = IndexServerAddress()
   121  		configFile.Configs[IndexServerAddress()] = authConfig
   122  	} else {
   123  		for k, authConfig := range configFile.Configs {
   124  			authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
   125  			if err != nil {
   126  				return &configFile, err
   127  			}
   128  			authConfig.Auth = ""
   129  			authConfig.ServerAddress = k
   130  			configFile.Configs[k] = authConfig
   131  		}
   132  	}
   133  	return &configFile, nil
   134  }
   135  
   136  // save the auth config
   137  func SaveConfig(configFile *ConfigFile) error {
   138  	confFile := path.Join(configFile.rootPath, CONFIGFILE)
   139  	if len(configFile.Configs) == 0 {
   140  		os.Remove(confFile)
   141  		return nil
   142  	}
   143  
   144  	configs := make(map[string]AuthConfig, len(configFile.Configs))
   145  	for k, authConfig := range configFile.Configs {
   146  		authCopy := authConfig
   147  
   148  		authCopy.Auth = encodeAuth(&authCopy)
   149  		authCopy.Username = ""
   150  		authCopy.Password = ""
   151  		authCopy.ServerAddress = ""
   152  		configs[k] = authCopy
   153  	}
   154  
   155  	b, err := json.Marshal(configs)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	err = ioutil.WriteFile(confFile, b, 0600)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	return nil
   164  }
   165  
   166  // try to register/login to the registry server
   167  func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) {
   168  	var (
   169  		status  string
   170  		reqBody []byte
   171  		err     error
   172  		client  = &http.Client{
   173  			Transport: &http.Transport{
   174  				DisableKeepAlives: true,
   175  				Proxy:             http.ProxyFromEnvironment,
   176  			},
   177  			CheckRedirect: AddRequiredHeadersToRedirectedRequests,
   178  		}
   179  		reqStatusCode = 0
   180  		serverAddress = authConfig.ServerAddress
   181  	)
   182  
   183  	if serverAddress == "" {
   184  		serverAddress = IndexServerAddress()
   185  	}
   186  
   187  	loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
   188  
   189  	// to avoid sending the server address to the server it should be removed before being marshalled
   190  	authCopy := *authConfig
   191  	authCopy.ServerAddress = ""
   192  
   193  	jsonBody, err := json.Marshal(authCopy)
   194  	if err != nil {
   195  		return "", fmt.Errorf("Config Error: %s", err)
   196  	}
   197  
   198  	// using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
   199  	b := strings.NewReader(string(jsonBody))
   200  	req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b)
   201  	if err != nil {
   202  		return "", fmt.Errorf("Server Error: %s", err)
   203  	}
   204  	reqStatusCode = req1.StatusCode
   205  	defer req1.Body.Close()
   206  	reqBody, err = ioutil.ReadAll(req1.Body)
   207  	if err != nil {
   208  		return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err)
   209  	}
   210  
   211  	if reqStatusCode == 201 {
   212  		if loginAgainstOfficialIndex {
   213  			status = "Account created. Please use the confirmation link we sent" +
   214  				" to your e-mail to activate it."
   215  		} else {
   216  			status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
   217  		}
   218  	} else if reqStatusCode == 400 {
   219  		if string(reqBody) == "\"Username or email already exists\"" {
   220  			req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
   221  			req.SetBasicAuth(authConfig.Username, authConfig.Password)
   222  			resp, err := client.Do(req)
   223  			if err != nil {
   224  				return "", err
   225  			}
   226  			defer resp.Body.Close()
   227  			body, err := ioutil.ReadAll(resp.Body)
   228  			if err != nil {
   229  				return "", err
   230  			}
   231  			if resp.StatusCode == 200 {
   232  				return "Login Succeeded", nil
   233  			} else if resp.StatusCode == 401 {
   234  				return "", fmt.Errorf("Wrong login/password, please try again")
   235  			} else if resp.StatusCode == 403 {
   236  				if loginAgainstOfficialIndex {
   237  					return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.")
   238  				}
   239  				return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
   240  			}
   241  			return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header)
   242  		}
   243  		return "", fmt.Errorf("Registration: %s", reqBody)
   244  
   245  	} else if reqStatusCode == 401 {
   246  		// This case would happen with private registries where /v1/users is
   247  		// protected, so people can use `docker login` as an auth check.
   248  		req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
   249  		req.SetBasicAuth(authConfig.Username, authConfig.Password)
   250  		resp, err := client.Do(req)
   251  		if err != nil {
   252  			return "", err
   253  		}
   254  		defer resp.Body.Close()
   255  		body, err := ioutil.ReadAll(resp.Body)
   256  		if err != nil {
   257  			return "", err
   258  		}
   259  		if resp.StatusCode == 200 {
   260  			return "Login Succeeded", nil
   261  		} else if resp.StatusCode == 401 {
   262  			return "", fmt.Errorf("Wrong login/password, please try again")
   263  		} else {
   264  			return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
   265  				resp.StatusCode, resp.Header)
   266  		}
   267  	} else {
   268  		return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
   269  	}
   270  	return status, nil
   271  }
   272  
   273  // this method matches a auth configuration to a server address or a url
   274  func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig {
   275  	if hostname == IndexServerAddress() || len(hostname) == 0 {
   276  		// default to the index server
   277  		return config.Configs[IndexServerAddress()]
   278  	}
   279  
   280  	// First try the happy case
   281  	if c, found := config.Configs[hostname]; found {
   282  		return c
   283  	}
   284  
   285  	convertToHostname := func(url string) string {
   286  		stripped := url
   287  		if strings.HasPrefix(url, "http://") {
   288  			stripped = strings.Replace(url, "http://", "", 1)
   289  		} else if strings.HasPrefix(url, "https://") {
   290  			stripped = strings.Replace(url, "https://", "", 1)
   291  		}
   292  
   293  		nameParts := strings.SplitN(stripped, "/", 2)
   294  
   295  		return nameParts[0]
   296  	}
   297  
   298  	// Maybe they have a legacy config file, we will iterate the keys converting
   299  	// them to the new format and testing
   300  	normalizedHostename := convertToHostname(hostname)
   301  	for registry, config := range config.Configs {
   302  		if registryHostname := convertToHostname(registry); registryHostname == normalizedHostename {
   303  			return config
   304  		}
   305  	}
   306  
   307  	// When all else fails, return an empty auth config
   308  	return AuthConfig{}
   309  }