commit
						fda85abaf9
					
				|  | @ -0,0 +1,290 @@ | ||||||
|  | package registry | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/dotcloud/docker/utils" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Where we store the config file
 | ||||||
|  | const CONFIGFILE = ".dockercfg" | ||||||
|  | 
 | ||||||
|  | // Only used for user auth + account creation
 | ||||||
|  | const INDEXSERVER = "https://index.docker.io/v1/" | ||||||
|  | 
 | ||||||
|  | //const INDEXSERVER = "https://indexstaging-docker.dotcloud.com/v1/"
 | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	ErrConfigFileMissing = errors.New("The Auth config file is missing") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type AuthConfig struct { | ||||||
|  | 	Username      string `json:"username,omitempty"` | ||||||
|  | 	Password      string `json:"password,omitempty"` | ||||||
|  | 	Auth          string `json:"auth"` | ||||||
|  | 	Email         string `json:"email"` | ||||||
|  | 	ServerAddress string `json:"serveraddress,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ConfigFile struct { | ||||||
|  | 	Configs  map[string]AuthConfig `json:"configs,omitempty"` | ||||||
|  | 	rootPath string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func IndexServerAddress() string { | ||||||
|  | 	return INDEXSERVER | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // create a base64 encoded auth string to store in config
 | ||||||
|  | func encodeAuth(authConfig *AuthConfig) string { | ||||||
|  | 	authStr := authConfig.Username + ":" + authConfig.Password | ||||||
|  | 	msg := []byte(authStr) | ||||||
|  | 	encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) | ||||||
|  | 	base64.StdEncoding.Encode(encoded, msg) | ||||||
|  | 	return string(encoded) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // decode the auth string
 | ||||||
|  | func decodeAuth(authStr string) (string, string, error) { | ||||||
|  | 	decLen := base64.StdEncoding.DecodedLen(len(authStr)) | ||||||
|  | 	decoded := make([]byte, decLen) | ||||||
|  | 	authByte := []byte(authStr) | ||||||
|  | 	n, err := base64.StdEncoding.Decode(decoded, authByte) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", "", err | ||||||
|  | 	} | ||||||
|  | 	if n > decLen { | ||||||
|  | 		return "", "", fmt.Errorf("Something went wrong decoding auth config") | ||||||
|  | 	} | ||||||
|  | 	arr := strings.SplitN(string(decoded), ":", 2) | ||||||
|  | 	if len(arr) != 2 { | ||||||
|  | 		return "", "", fmt.Errorf("Invalid auth configuration file") | ||||||
|  | 	} | ||||||
|  | 	password := strings.Trim(arr[1], "\x00") | ||||||
|  | 	return arr[0], password, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // load up the auth config information and return values
 | ||||||
|  | // FIXME: use the internal golang config parser
 | ||||||
|  | func LoadConfig(rootPath string) (*ConfigFile, error) { | ||||||
|  | 	configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath} | ||||||
|  | 	confFile := path.Join(rootPath, CONFIGFILE) | ||||||
|  | 	if _, err := os.Stat(confFile); err != nil { | ||||||
|  | 		return &configFile, nil //missing file is not an error
 | ||||||
|  | 	} | ||||||
|  | 	b, err := ioutil.ReadFile(confFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return &configFile, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := json.Unmarshal(b, &configFile.Configs); err != nil { | ||||||
|  | 		arr := strings.Split(string(b), "\n") | ||||||
|  | 		if len(arr) < 2 { | ||||||
|  | 			return &configFile, fmt.Errorf("The Auth config file is empty") | ||||||
|  | 		} | ||||||
|  | 		authConfig := AuthConfig{} | ||||||
|  | 		origAuth := strings.Split(arr[0], " = ") | ||||||
|  | 		if len(origAuth) != 2 { | ||||||
|  | 			return &configFile, fmt.Errorf("Invalid Auth config file") | ||||||
|  | 		} | ||||||
|  | 		authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return &configFile, err | ||||||
|  | 		} | ||||||
|  | 		origEmail := strings.Split(arr[1], " = ") | ||||||
|  | 		if len(origEmail) != 2 { | ||||||
|  | 			return &configFile, fmt.Errorf("Invalid Auth config file") | ||||||
|  | 		} | ||||||
|  | 		authConfig.Email = origEmail[1] | ||||||
|  | 		authConfig.ServerAddress = IndexServerAddress() | ||||||
|  | 		configFile.Configs[IndexServerAddress()] = authConfig | ||||||
|  | 	} else { | ||||||
|  | 		for k, authConfig := range configFile.Configs { | ||||||
|  | 			authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return &configFile, err | ||||||
|  | 			} | ||||||
|  | 			authConfig.Auth = "" | ||||||
|  | 			configFile.Configs[k] = authConfig | ||||||
|  | 			authConfig.ServerAddress = k | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return &configFile, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // save the auth config
 | ||||||
|  | func SaveConfig(configFile *ConfigFile) error { | ||||||
|  | 	confFile := path.Join(configFile.rootPath, CONFIGFILE) | ||||||
|  | 	if len(configFile.Configs) == 0 { | ||||||
|  | 		os.Remove(confFile) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	configs := make(map[string]AuthConfig, len(configFile.Configs)) | ||||||
|  | 	for k, authConfig := range configFile.Configs { | ||||||
|  | 		authCopy := authConfig | ||||||
|  | 
 | ||||||
|  | 		authCopy.Auth = encodeAuth(&authCopy) | ||||||
|  | 		authCopy.Username = "" | ||||||
|  | 		authCopy.Password = "" | ||||||
|  | 		authCopy.ServerAddress = "" | ||||||
|  | 		configs[k] = authCopy | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	b, err := json.Marshal(configs) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	err = ioutil.WriteFile(confFile, b, 0600) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // try to register/login to the registry server
 | ||||||
|  | func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) { | ||||||
|  | 	var ( | ||||||
|  | 		status        string | ||||||
|  | 		reqBody       []byte | ||||||
|  | 		err           error | ||||||
|  | 		client        = &http.Client{} | ||||||
|  | 		reqStatusCode = 0 | ||||||
|  | 		serverAddress = authConfig.ServerAddress | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	if serverAddress == "" { | ||||||
|  | 		serverAddress = IndexServerAddress() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	loginAgainstOfficialIndex := serverAddress == IndexServerAddress() | ||||||
|  | 
 | ||||||
|  | 	// to avoid sending the server address to the server it should be removed before being marshalled
 | ||||||
|  | 	authCopy := *authConfig | ||||||
|  | 	authCopy.ServerAddress = "" | ||||||
|  | 
 | ||||||
|  | 	jsonBody, err := json.Marshal(authCopy) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("Config Error: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
 | ||||||
|  | 	b := strings.NewReader(string(jsonBody)) | ||||||
|  | 	req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("Server Error: %s", err) | ||||||
|  | 	} | ||||||
|  | 	reqStatusCode = req1.StatusCode | ||||||
|  | 	defer req1.Body.Close() | ||||||
|  | 	reqBody, err = ioutil.ReadAll(req1.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if reqStatusCode == 201 { | ||||||
|  | 		if loginAgainstOfficialIndex { | ||||||
|  | 			status = "Account created. Please use the confirmation link we sent" + | ||||||
|  | 				" to your e-mail to activate it." | ||||||
|  | 		} else { | ||||||
|  | 			status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it." | ||||||
|  | 		} | ||||||
|  | 	} else if reqStatusCode == 400 { | ||||||
|  | 		if string(reqBody) == "\"Username or email already exists\"" { | ||||||
|  | 			req, err := factory.NewRequest("GET", serverAddress+"users/", nil) | ||||||
|  | 			req.SetBasicAuth(authConfig.Username, authConfig.Password) | ||||||
|  | 			resp, err := client.Do(req) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return "", err | ||||||
|  | 			} | ||||||
|  | 			defer resp.Body.Close() | ||||||
|  | 			body, err := ioutil.ReadAll(resp.Body) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return "", err | ||||||
|  | 			} | ||||||
|  | 			if resp.StatusCode == 200 { | ||||||
|  | 				status = "Login Succeeded" | ||||||
|  | 			} else if resp.StatusCode == 401 { | ||||||
|  | 				return "", fmt.Errorf("Wrong login/password, please try again") | ||||||
|  | 			} else if resp.StatusCode == 403 { | ||||||
|  | 				if loginAgainstOfficialIndex { | ||||||
|  | 					return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.") | ||||||
|  | 				} | ||||||
|  | 				return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) | ||||||
|  | 			} else { | ||||||
|  | 				return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			return "", fmt.Errorf("Registration: %s", reqBody) | ||||||
|  | 		} | ||||||
|  | 	} else if reqStatusCode == 401 { | ||||||
|  | 		// This case would happen with private registries where /v1/users is
 | ||||||
|  | 		// protected, so people can use `docker login` as an auth check.
 | ||||||
|  | 		req, err := factory.NewRequest("GET", serverAddress+"users/", nil) | ||||||
|  | 		req.SetBasicAuth(authConfig.Username, authConfig.Password) | ||||||
|  | 		resp, err := client.Do(req) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 		defer resp.Body.Close() | ||||||
|  | 		body, err := ioutil.ReadAll(resp.Body) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 		if resp.StatusCode == 200 { | ||||||
|  | 			status = "Login Succeeded" | ||||||
|  | 		} else if resp.StatusCode == 401 { | ||||||
|  | 			return "", fmt.Errorf("Wrong login/password, please try again") | ||||||
|  | 		} else { | ||||||
|  | 			return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, | ||||||
|  | 				resp.StatusCode, resp.Header) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody) | ||||||
|  | 	} | ||||||
|  | 	return status, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // this method matches a auth configuration to a server address or a url
 | ||||||
|  | func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig { | ||||||
|  | 	if hostname == IndexServerAddress() || len(hostname) == 0 { | ||||||
|  | 		// default to the index server
 | ||||||
|  | 		return config.Configs[IndexServerAddress()] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// First try the happy case
 | ||||||
|  | 	if c, found := config.Configs[hostname]; found { | ||||||
|  | 		return c | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	convertToHostname := func(url string) string { | ||||||
|  | 		stripped := url | ||||||
|  | 		if strings.HasPrefix(url, "http://") { | ||||||
|  | 			stripped = strings.Replace(url, "http://", "", 1) | ||||||
|  | 		} else if strings.HasPrefix(url, "https://") { | ||||||
|  | 			stripped = strings.Replace(url, "https://", "", 1) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		nameParts := strings.SplitN(stripped, "/", 2) | ||||||
|  | 
 | ||||||
|  | 		return nameParts[0] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Maybe they have a legacy config file, we will iterate the keys converting
 | ||||||
|  | 	// them to the new format and testing
 | ||||||
|  | 	normalizedHostename := convertToHostname(hostname) | ||||||
|  | 	for registry, config := range config.Configs { | ||||||
|  | 		if registryHostname := convertToHostname(registry); registryHostname == normalizedHostename { | ||||||
|  | 			return config | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// When all else fails, return an empty auth config
 | ||||||
|  | 	return AuthConfig{} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,149 @@ | ||||||
|  | package registry | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestEncodeAuth(t *testing.T) { | ||||||
|  | 	newAuthConfig := &AuthConfig{Username: "ken", Password: "test", Email: "test@example.com"} | ||||||
|  | 	authStr := encodeAuth(newAuthConfig) | ||||||
|  | 	decAuthConfig := &AuthConfig{} | ||||||
|  | 	var err error | ||||||
|  | 	decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	if newAuthConfig.Username != decAuthConfig.Username { | ||||||
|  | 		t.Fatal("Encode Username doesn't match decoded Username") | ||||||
|  | 	} | ||||||
|  | 	if newAuthConfig.Password != decAuthConfig.Password { | ||||||
|  | 		t.Fatal("Encode Password doesn't match decoded Password") | ||||||
|  | 	} | ||||||
|  | 	if authStr != "a2VuOnRlc3Q=" { | ||||||
|  | 		t.Fatal("AuthString encoding isn't correct.") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func setupTempConfigFile() (*ConfigFile, error) { | ||||||
|  | 	root, err := ioutil.TempDir("", "docker-test-auth") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	configFile := &ConfigFile{ | ||||||
|  | 		rootPath: root, | ||||||
|  | 		Configs:  make(map[string]AuthConfig), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, registry := range []string{"testIndex", IndexServerAddress()} { | ||||||
|  | 		configFile.Configs[registry] = AuthConfig{ | ||||||
|  | 			Username: "docker-user", | ||||||
|  | 			Password: "docker-pass", | ||||||
|  | 			Email:    "docker@docker.io", | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return configFile, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestSameAuthDataPostSave(t *testing.T) { | ||||||
|  | 	configFile, err := setupTempConfigFile() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	defer os.RemoveAll(configFile.rootPath) | ||||||
|  | 
 | ||||||
|  | 	err = SaveConfig(configFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	authConfig := configFile.Configs["testIndex"] | ||||||
|  | 	if authConfig.Username != "docker-user" { | ||||||
|  | 		t.Fail() | ||||||
|  | 	} | ||||||
|  | 	if authConfig.Password != "docker-pass" { | ||||||
|  | 		t.Fail() | ||||||
|  | 	} | ||||||
|  | 	if authConfig.Email != "docker@docker.io" { | ||||||
|  | 		t.Fail() | ||||||
|  | 	} | ||||||
|  | 	if authConfig.Auth != "" { | ||||||
|  | 		t.Fail() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestResolveAuthConfigIndexServer(t *testing.T) { | ||||||
|  | 	configFile, err := setupTempConfigFile() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	defer os.RemoveAll(configFile.rootPath) | ||||||
|  | 
 | ||||||
|  | 	for _, registry := range []string{"", IndexServerAddress()} { | ||||||
|  | 		resolved := configFile.ResolveAuthConfig(registry) | ||||||
|  | 		if resolved != configFile.Configs[IndexServerAddress()] { | ||||||
|  | 			t.Fail() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestResolveAuthConfigFullURL(t *testing.T) { | ||||||
|  | 	configFile, err := setupTempConfigFile() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	defer os.RemoveAll(configFile.rootPath) | ||||||
|  | 
 | ||||||
|  | 	registryAuth := AuthConfig{ | ||||||
|  | 		Username: "foo-user", | ||||||
|  | 		Password: "foo-pass", | ||||||
|  | 		Email:    "foo@example.com", | ||||||
|  | 	} | ||||||
|  | 	localAuth := AuthConfig{ | ||||||
|  | 		Username: "bar-user", | ||||||
|  | 		Password: "bar-pass", | ||||||
|  | 		Email:    "bar@example.com", | ||||||
|  | 	} | ||||||
|  | 	configFile.Configs["https://registry.example.com/v1/"] = registryAuth | ||||||
|  | 	configFile.Configs["http://localhost:8000/v1/"] = localAuth | ||||||
|  | 	configFile.Configs["registry.com"] = registryAuth | ||||||
|  | 
 | ||||||
|  | 	validRegistries := map[string][]string{ | ||||||
|  | 		"https://registry.example.com/v1/": { | ||||||
|  | 			"https://registry.example.com/v1/", | ||||||
|  | 			"http://registry.example.com/v1/", | ||||||
|  | 			"registry.example.com", | ||||||
|  | 			"registry.example.com/v1/", | ||||||
|  | 		}, | ||||||
|  | 		"http://localhost:8000/v1/": { | ||||||
|  | 			"https://localhost:8000/v1/", | ||||||
|  | 			"http://localhost:8000/v1/", | ||||||
|  | 			"localhost:8000", | ||||||
|  | 			"localhost:8000/v1/", | ||||||
|  | 		}, | ||||||
|  | 		"registry.com": { | ||||||
|  | 			"https://registry.com/v1/", | ||||||
|  | 			"http://registry.com/v1/", | ||||||
|  | 			"registry.com", | ||||||
|  | 			"registry.com/v1/", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for configKey, registries := range validRegistries { | ||||||
|  | 		for _, registry := range registries { | ||||||
|  | 			var ( | ||||||
|  | 				configured AuthConfig | ||||||
|  | 				ok         bool | ||||||
|  | 			) | ||||||
|  | 			resolved := configFile.ResolveAuthConfig(registry) | ||||||
|  | 			if configured, ok = configFile.Configs[configKey]; !ok { | ||||||
|  | 				t.Fail() | ||||||
|  | 			} | ||||||
|  | 			if resolved.Email != configured.Email { | ||||||
|  | 				t.Errorf("%s -> %q != %q\n", registry, resolved.Email, configured.Email) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -6,7 +6,6 @@ import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/dotcloud/docker/auth" |  | ||||||
| 	"github.com/dotcloud/docker/utils" | 	"github.com/dotcloud/docker/utils" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
|  | @ -27,7 +26,7 @@ var ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func pingRegistryEndpoint(endpoint string) (bool, error) { | func pingRegistryEndpoint(endpoint string) (bool, error) { | ||||||
| 	if endpoint == auth.IndexServerAddress() { | 	if endpoint == IndexServerAddress() { | ||||||
| 		// Skip the check, we now this one is valid
 | 		// Skip the check, we now this one is valid
 | ||||||
| 		// (and we never want to fallback to http in case of error)
 | 		// (and we never want to fallback to http in case of error)
 | ||||||
| 		return false, nil | 		return false, nil | ||||||
|  | @ -42,7 +41,10 @@ func pingRegistryEndpoint(endpoint string) (bool, error) { | ||||||
| 		conn.SetDeadline(time.Now().Add(time.Duration(10) * time.Second)) | 		conn.SetDeadline(time.Now().Add(time.Duration(10) * time.Second)) | ||||||
| 		return conn, nil | 		return conn, nil | ||||||
| 	} | 	} | ||||||
| 	httpTransport := &http.Transport{Dial: httpDial} | 	httpTransport := &http.Transport{ | ||||||
|  | 		Dial:  httpDial, | ||||||
|  | 		Proxy: http.ProxyFromEnvironment, | ||||||
|  | 	} | ||||||
| 	client := &http.Client{Transport: httpTransport} | 	client := &http.Client{Transport: httpTransport} | ||||||
| 	resp, err := client.Get(endpoint + "_ping") | 	resp, err := client.Get(endpoint + "_ping") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -103,7 +105,7 @@ func ResolveRepositoryName(reposName string) (string, string, error) { | ||||||
| 		nameParts[0] != "localhost" { | 		nameParts[0] != "localhost" { | ||||||
| 		// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
 | 		// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
 | ||||||
| 		err := validateRepositoryName(reposName) | 		err := validateRepositoryName(reposName) | ||||||
| 		return auth.IndexServerAddress(), reposName, err | 		return IndexServerAddress(), reposName, err | ||||||
| 	} | 	} | ||||||
| 	if len(nameParts) < 2 { | 	if len(nameParts) < 2 { | ||||||
| 		// There is a dot in repos name (and no registry address)
 | 		// There is a dot in repos name (and no registry address)
 | ||||||
|  | @ -149,20 +151,6 @@ func ExpandAndVerifyRegistryUrl(hostname string) (string, error) { | ||||||
| 	return endpoint, nil | 	return endpoint, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) { |  | ||||||
| 	for _, cookie := range c.Jar.Cookies(req.URL) { |  | ||||||
| 		req.AddCookie(cookie) |  | ||||||
| 	} |  | ||||||
| 	res, err := c.Do(req) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if len(res.Cookies()) > 0 { |  | ||||||
| 		c.Jar.SetCookies(req.URL, res.Cookies()) |  | ||||||
| 	} |  | ||||||
| 	return res, err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func setTokenAuth(req *http.Request, token []string) { | func setTokenAuth(req *http.Request, token []string) { | ||||||
| 	if req.Header.Get("Authorization") == "" { // Don't override
 | 	if req.Header.Get("Authorization") == "" { // Don't override
 | ||||||
| 		req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) | 		req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) | ||||||
|  | @ -177,7 +165,7 @@ func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]s | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	setTokenAuth(req, token) | 	setTokenAuth(req, token) | ||||||
| 	res, err := doWithCookies(r.client, req) | 	res, err := r.client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -212,7 +200,7 @@ func (r *Registry) LookupRemoteImage(imgID, registry string, token []string) boo | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	setTokenAuth(req, token) | 	setTokenAuth(req, token) | ||||||
| 	res, err := doWithCookies(r.client, req) | 	res, err := r.client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		utils.Errorf("Error in LookupRemoteImage %s", err) | 		utils.Errorf("Error in LookupRemoteImage %s", err) | ||||||
| 		return false | 		return false | ||||||
|  | @ -229,7 +217,7 @@ func (r *Registry) GetRemoteImageJSON(imgID, registry string, token []string) ([ | ||||||
| 		return nil, -1, fmt.Errorf("Failed to download json: %s", err) | 		return nil, -1, fmt.Errorf("Failed to download json: %s", err) | ||||||
| 	} | 	} | ||||||
| 	setTokenAuth(req, token) | 	setTokenAuth(req, token) | ||||||
| 	res, err := doWithCookies(r.client, req) | 	res, err := r.client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, -1, fmt.Errorf("Failed to download json: %s", err) | 		return nil, -1, fmt.Errorf("Failed to download json: %s", err) | ||||||
| 	} | 	} | ||||||
|  | @ -256,7 +244,7 @@ func (r *Registry) GetRemoteImageLayer(imgID, registry string, token []string) ( | ||||||
| 		return nil, fmt.Errorf("Error while getting from the server: %s\n", err) | 		return nil, fmt.Errorf("Error while getting from the server: %s\n", err) | ||||||
| 	} | 	} | ||||||
| 	setTokenAuth(req, token) | 	setTokenAuth(req, token) | ||||||
| 	res, err := doWithCookies(r.client, req) | 	res, err := r.client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -282,7 +270,7 @@ func (r *Registry) GetRemoteTags(registries []string, repository string, token [ | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		setTokenAuth(req, token) | 		setTokenAuth(req, token) | ||||||
| 		res, err := doWithCookies(r.client, req) | 		res, err := r.client.Do(req) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  | @ -388,7 +376,7 @@ func (r *Registry) PushImageChecksumRegistry(imgData *ImgData, registry string, | ||||||
| 	req.Header.Set("X-Docker-Checksum", imgData.Checksum) | 	req.Header.Set("X-Docker-Checksum", imgData.Checksum) | ||||||
| 	req.Header.Set("X-Docker-Checksum-Payload", imgData.ChecksumPayload) | 	req.Header.Set("X-Docker-Checksum-Payload", imgData.ChecksumPayload) | ||||||
| 
 | 
 | ||||||
| 	res, err := doWithCookies(r.client, req) | 	res, err := r.client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("Failed to upload metadata: %s", err) | 		return fmt.Errorf("Failed to upload metadata: %s", err) | ||||||
| 	} | 	} | ||||||
|  | @ -424,11 +412,14 @@ func (r *Registry) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, regis | ||||||
| 	req.Header.Add("Content-type", "application/json") | 	req.Header.Add("Content-type", "application/json") | ||||||
| 	setTokenAuth(req, token) | 	setTokenAuth(req, token) | ||||||
| 
 | 
 | ||||||
| 	res, err := doWithCookies(r.client, req) | 	res, err := r.client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("Failed to upload metadata: %s", err) | 		return fmt.Errorf("Failed to upload metadata: %s", err) | ||||||
| 	} | 	} | ||||||
| 	defer res.Body.Close() | 	defer res.Body.Close() | ||||||
|  | 	if res.StatusCode == 401 && strings.HasPrefix(registry, "http://") { | ||||||
|  | 		return utils.NewHTTPRequestError("HTTP code 401, Docker will not send auth headers over HTTP.", res) | ||||||
|  | 	} | ||||||
| 	if res.StatusCode != 200 { | 	if res.StatusCode != 200 { | ||||||
| 		errBody, err := ioutil.ReadAll(res.Body) | 		errBody, err := ioutil.ReadAll(res.Body) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -449,18 +440,20 @@ func (r *Registry) PushImageLayerRegistry(imgID string, layer io.Reader, registr | ||||||
| 
 | 
 | ||||||
| 	utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgID+"/layer") | 	utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgID+"/layer") | ||||||
| 
 | 
 | ||||||
|  | 	tarsumLayer := &utils.TarSum{Reader: layer} | ||||||
| 	h := sha256.New() | 	h := sha256.New() | ||||||
| 	checksumLayer := &utils.CheckSum{Reader: layer, Hash: h} | 	h.Write(jsonRaw) | ||||||
| 	tarsumLayer := &utils.TarSum{Reader: checksumLayer} | 	h.Write([]byte{'\n'}) | ||||||
|  | 	checksumLayer := &utils.CheckSum{Reader: tarsumLayer, Hash: h} | ||||||
| 
 | 
 | ||||||
| 	req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgID+"/layer", tarsumLayer) | 	req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgID+"/layer", checksumLayer) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", "", err | 		return "", "", err | ||||||
| 	} | 	} | ||||||
| 	req.ContentLength = -1 | 	req.ContentLength = -1 | ||||||
| 	req.TransferEncoding = []string{"chunked"} | 	req.TransferEncoding = []string{"chunked"} | ||||||
| 	setTokenAuth(req, token) | 	setTokenAuth(req, token) | ||||||
| 	res, err := doWithCookies(r.client, req) | 	res, err := r.client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", "", fmt.Errorf("Failed to upload layer: %s", err) | 		return "", "", fmt.Errorf("Failed to upload layer: %s", err) | ||||||
| 	} | 	} | ||||||
|  | @ -497,7 +490,7 @@ func (r *Registry) PushRegistryTag(remote, revision, tag, registry string, token | ||||||
| 	req.Header.Add("Content-type", "application/json") | 	req.Header.Add("Content-type", "application/json") | ||||||
| 	setTokenAuth(req, token) | 	setTokenAuth(req, token) | ||||||
| 	req.ContentLength = int64(len(revision)) | 	req.ContentLength = int64(len(revision)) | ||||||
| 	res, err := doWithCookies(r.client, req) | 	res, err := r.client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | @ -615,7 +608,7 @@ func (r *Registry) PushImageJSONIndex(remote string, imgList []*ImgData, validat | ||||||
| 
 | 
 | ||||||
| func (r *Registry) SearchRepositories(term string) (*SearchResults, error) { | func (r *Registry) SearchRepositories(term string) (*SearchResults, error) { | ||||||
| 	utils.Debugf("Index server: %s", r.indexEndpoint) | 	utils.Debugf("Index server: %s", r.indexEndpoint) | ||||||
| 	u := auth.IndexServerAddress() + "search?q=" + url.QueryEscape(term) | 	u := r.indexEndpoint + "search?q=" + url.QueryEscape(term) | ||||||
| 	req, err := r.reqFactory.NewRequest("GET", u, nil) | 	req, err := r.reqFactory.NewRequest("GET", u, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -641,12 +634,12 @@ func (r *Registry) SearchRepositories(term string) (*SearchResults, error) { | ||||||
| 	return result, err | 	return result, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *Registry) GetAuthConfig(withPasswd bool) *auth.AuthConfig { | func (r *Registry) GetAuthConfig(withPasswd bool) *AuthConfig { | ||||||
| 	password := "" | 	password := "" | ||||||
| 	if withPasswd { | 	if withPasswd { | ||||||
| 		password = r.authConfig.Password | 		password = r.authConfig.Password | ||||||
| 	} | 	} | ||||||
| 	return &auth.AuthConfig{ | 	return &AuthConfig{ | ||||||
| 		Username: r.authConfig.Username, | 		Username: r.authConfig.Username, | ||||||
| 		Password: password, | 		Password: password, | ||||||
| 		Email:    r.authConfig.Email, | 		Email:    r.authConfig.Email, | ||||||
|  | @ -682,12 +675,12 @@ type ImgData struct { | ||||||
| 
 | 
 | ||||||
| type Registry struct { | type Registry struct { | ||||||
| 	client        *http.Client | 	client        *http.Client | ||||||
| 	authConfig    *auth.AuthConfig | 	authConfig    *AuthConfig | ||||||
| 	reqFactory    *utils.HTTPRequestFactory | 	reqFactory    *utils.HTTPRequestFactory | ||||||
| 	indexEndpoint string | 	indexEndpoint string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewRegistry(authConfig *auth.AuthConfig, factory *utils.HTTPRequestFactory, indexEndpoint string) (r *Registry, err error) { | func NewRegistry(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, indexEndpoint string) (r *Registry, err error) { | ||||||
| 	httpTransport := &http.Transport{ | 	httpTransport := &http.Transport{ | ||||||
| 		DisableKeepAlives: true, | 		DisableKeepAlives: true, | ||||||
| 		Proxy:             http.ProxyFromEnvironment, | 		Proxy:             http.ProxyFromEnvironment, | ||||||
|  | @ -707,13 +700,13 @@ func NewRegistry(authConfig *auth.AuthConfig, factory *utils.HTTPRequestFactory, | ||||||
| 
 | 
 | ||||||
| 	// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
 | 	// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
 | ||||||
| 	// alongside our requests.
 | 	// alongside our requests.
 | ||||||
| 	if indexEndpoint != auth.IndexServerAddress() && strings.HasPrefix(indexEndpoint, "https://") { | 	if indexEndpoint != IndexServerAddress() && strings.HasPrefix(indexEndpoint, "https://") { | ||||||
| 		standalone, err := pingRegistryEndpoint(indexEndpoint) | 		standalone, err := pingRegistryEndpoint(indexEndpoint) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		if standalone { | 		if standalone { | ||||||
| 			utils.Debugf("Endpoint %s is eligible for private registry auth. Enabling decorator.", indexEndpoint) | 			utils.Debugf("Endpoint %s is eligible for private registry registry. Enabling decorator.", indexEndpoint) | ||||||
| 			dec := utils.NewHTTPAuthDecorator(authConfig.Username, authConfig.Password) | 			dec := utils.NewHTTPAuthDecorator(authConfig.Username, authConfig.Password) | ||||||
| 			factory.AddDecorator(dec) | 			factory.AddDecorator(dec) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -321,7 +321,12 @@ func handlerAuth(w http.ResponseWriter, r *http.Request) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func handlerSearch(w http.ResponseWriter, r *http.Request) { | func handlerSearch(w http.ResponseWriter, r *http.Request) { | ||||||
| 	writeResponse(w, "{}", 200) | 	result := &SearchResults{ | ||||||
|  | 		Query:      "fakequery", | ||||||
|  | 		NumResults: 1, | ||||||
|  | 		Results:    []SearchResult{{Name: "fakeimage", StarCount: 42}}, | ||||||
|  | 	} | ||||||
|  | 	writeResponse(w, result, 200) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestPing(t *testing.T) { | func TestPing(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| package registry | package registry | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/dotcloud/docker/auth" |  | ||||||
| 	"github.com/dotcloud/docker/utils" | 	"github.com/dotcloud/docker/utils" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | @ -14,7 +13,7 @@ var ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func spawnTestRegistry(t *testing.T) *Registry { | func spawnTestRegistry(t *testing.T) *Registry { | ||||||
| 	authConfig := &auth.AuthConfig{} | 	authConfig := &AuthConfig{} | ||||||
| 	r, err := NewRegistry(authConfig, utils.NewHTTPRequestFactory(), makeURL("/v1/")) | 	r, err := NewRegistry(authConfig, utils.NewHTTPRequestFactory(), makeURL("/v1/")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
|  | @ -137,7 +136,7 @@ func TestResolveRepositoryName(t *testing.T) { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	assertEqual(t, ep, auth.IndexServerAddress(), "Expected endpoint to be index server address") | 	assertEqual(t, ep, IndexServerAddress(), "Expected endpoint to be index server address") | ||||||
| 	assertEqual(t, repo, "fooo/bar", "Expected resolved repo to be foo/bar") | 	assertEqual(t, repo, "fooo/bar", "Expected resolved repo to be foo/bar") | ||||||
| 
 | 
 | ||||||
| 	u := makeURL("")[7:] | 	u := makeURL("")[7:] | ||||||
|  | @ -187,14 +186,16 @@ func TestPushImageJSONIndex(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| func TestSearchRepositories(t *testing.T) { | func TestSearchRepositories(t *testing.T) { | ||||||
| 	r := spawnTestRegistry(t) | 	r := spawnTestRegistry(t) | ||||||
| 	results, err := r.SearchRepositories("supercalifragilisticepsialidocious") | 	results, err := r.SearchRepositories("fakequery") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	if results == nil { | 	if results == nil { | ||||||
| 		t.Fatal("Expected non-nil SearchResults object") | 		t.Fatal("Expected non-nil SearchResults object") | ||||||
| 	} | 	} | ||||||
| 	assertEqual(t, results.NumResults, 0, "Expected 0 search results") | 	assertEqual(t, results.NumResults, 1, "Expected 1 search results") | ||||||
|  | 	assertEqual(t, results.Query, "fakequery", "Expected 'fakequery' as query") | ||||||
|  | 	assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' a ot hae 42 stars") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestValidRepositoryName(t *testing.T) { | func TestValidRepositoryName(t *testing.T) { | ||||||
|  | @ -205,4 +206,8 @@ func TestValidRepositoryName(t *testing.T) { | ||||||
| 		t.Log("Repository name should be invalid") | 		t.Log("Repository name should be invalid") | ||||||
| 		t.Fail() | 		t.Fail() | ||||||
| 	} | 	} | ||||||
|  | 	if err := validateRepositoryName("docker///docker"); err == nil { | ||||||
|  | 		t.Log("Repository name should be invalid") | ||||||
|  | 		t.Fail() | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue