package cliconfig import ( "encoding/base64" "encoding/json" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/homedir" ) const ( // ConfigFileName is the name of config file ConfigFileName = "config.json" oldConfigfile = ".dockercfg" // This constant is only used for really old config files when the // URL wasn't saved as part of the config file and it was just // assumed to be this value. defaultIndexserver = "https://index.docker.io/v1/" ) var ( configDir = os.Getenv("DOCKER_CONFIG") ) func init() { if configDir == "" { configDir = filepath.Join(homedir.Get(), ".docker") } } // ConfigDir returns the directory the configuration file is stored in func ConfigDir() string { return configDir } // SetConfigDir sets the directory the configuration file is stored in func SetConfigDir(dir string) { configDir = dir } // ConfigFile ~/.docker/config.json file info type ConfigFile struct { AuthConfigs map[string]types.AuthConfig `json:"auths"` HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` PsFormat string `json:"psFormat,omitempty"` filename string // Note: not serialized - for internal use only } // NewConfigFile initializes an empty configuration file for the given filename 'fn' func NewConfigFile(fn string) *ConfigFile { return &ConfigFile{ AuthConfigs: make(map[string]types.AuthConfig), HTTPHeaders: make(map[string]string), filename: fn, } } // LegacyLoadFromReader reads the non-nested configuration data given and sets up the // auth config information with given directory and populates the receiver object func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error { b, err := ioutil.ReadAll(configData) if err != nil { return err } if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil { arr := strings.Split(string(b), "\n") if len(arr) < 2 { return fmt.Errorf("The Auth config file is empty") } authConfig := types.AuthConfig{} origAuth := strings.Split(arr[0], " = ") if len(origAuth) != 2 { return fmt.Errorf("Invalid Auth config file") } authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) if err != nil { return err } origEmail := strings.Split(arr[1], " = ") if len(origEmail) != 2 { return fmt.Errorf("Invalid Auth config file") } authConfig.Email = origEmail[1] authConfig.ServerAddress = defaultIndexserver configFile.AuthConfigs[defaultIndexserver] = authConfig } else { for k, authConfig := range configFile.AuthConfigs { authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) if err != nil { return err } authConfig.Auth = "" authConfig.ServerAddress = k configFile.AuthConfigs[k] = authConfig } } return nil } // LoadFromReader reads the configuration data given and sets up the auth config // information with given directory and populates the receiver object func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error { if err := json.NewDecoder(configData).Decode(&configFile); err != nil { return err } var err error for addr, ac := range configFile.AuthConfigs { ac.Username, ac.Password, err = decodeAuth(ac.Auth) if err != nil { return err } ac.Auth = "" ac.ServerAddress = addr configFile.AuthConfigs[addr] = ac } return nil } // LegacyLoadFromReader is a convenience function that creates a ConfigFile object from // a non-nested reader func LegacyLoadFromReader(configData io.Reader) (*ConfigFile, error) { configFile := ConfigFile{ AuthConfigs: make(map[string]types.AuthConfig), } err := configFile.LegacyLoadFromReader(configData) return &configFile, err } // LoadFromReader is a convenience function that creates a ConfigFile object from // a reader func LoadFromReader(configData io.Reader) (*ConfigFile, error) { configFile := ConfigFile{ AuthConfigs: make(map[string]types.AuthConfig), } err := configFile.LoadFromReader(configData) return &configFile, err } // Load reads the configuration files in the given directory, and sets up // the auth config information and return values. // FIXME: use the internal golang config parser func Load(configDir string) (*ConfigFile, error) { if configDir == "" { configDir = ConfigDir() } configFile := ConfigFile{ AuthConfigs: make(map[string]types.AuthConfig), filename: filepath.Join(configDir, ConfigFileName), } // Try happy path first - latest config file if _, err := os.Stat(configFile.filename); err == nil { file, err := os.Open(configFile.filename) if err != nil { return &configFile, err } defer file.Close() err = configFile.LoadFromReader(file) return &configFile, err } else if !os.IsNotExist(err) { // if file is there but we can't stat it for any reason other // than it doesn't exist then stop return &configFile, err } // Can't find latest config file so check for the old one confFile := filepath.Join(homedir.Get(), oldConfigfile) if _, err := os.Stat(confFile); err != nil { return &configFile, nil //missing file is not an error } file, err := os.Open(confFile) if err != nil { return &configFile, err } defer file.Close() err = configFile.LegacyLoadFromReader(file) if err != nil { return &configFile, err } if configFile.HTTPHeaders == nil { configFile.HTTPHeaders = map[string]string{} } return &configFile, nil } // SaveToWriter encodes and writes out all the authorization information to // the given writer func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { // Encode sensitive data into a new/temp struct tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs)) for k, authConfig := range configFile.AuthConfigs { authCopy := authConfig // encode and save the authstring, while blanking out the original fields authCopy.Auth = encodeAuth(&authCopy) authCopy.Username = "" authCopy.Password = "" authCopy.ServerAddress = "" tmpAuthConfigs[k] = authCopy } saveAuthConfigs := configFile.AuthConfigs configFile.AuthConfigs = tmpAuthConfigs defer func() { configFile.AuthConfigs = saveAuthConfigs }() data, err := json.MarshalIndent(configFile, "", "\t") if err != nil { return err } _, err = writer.Write(data) return err } // Save encodes and writes out all the authorization information func (configFile *ConfigFile) Save() error { if configFile.Filename() == "" { return fmt.Errorf("Can't save config with empty filename") } if err := os.MkdirAll(filepath.Dir(configFile.filename), 0700); err != nil { return err } f, err := os.OpenFile(configFile.filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } defer f.Close() return configFile.SaveToWriter(f) } // Filename returns the name of the configuration file func (configFile *ConfigFile) Filename() string { return configFile.filename } // encodeAuth creates a base64 encoded string to containing authorization information func encodeAuth(authConfig *types.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) } // decodeAuth decodes a base64 encoded string and returns username and password 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 }