github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/cli/config/configfile/file.go (about) 1 package configfile 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/docker/cli/cli/config/credentials" 12 "github.com/docker/cli/cli/config/types" 13 "github.com/pkg/errors" 14 "github.com/sirupsen/logrus" 15 ) 16 17 // ConfigFile ~/.docker/config.json file info 18 type ConfigFile struct { 19 AuthConfigs map[string]types.AuthConfig `json:"auths"` 20 HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` 21 PsFormat string `json:"psFormat,omitempty"` 22 ImagesFormat string `json:"imagesFormat,omitempty"` 23 NetworksFormat string `json:"networksFormat,omitempty"` 24 PluginsFormat string `json:"pluginsFormat,omitempty"` 25 VolumesFormat string `json:"volumesFormat,omitempty"` 26 StatsFormat string `json:"statsFormat,omitempty"` 27 DetachKeys string `json:"detachKeys,omitempty"` 28 CredentialsStore string `json:"credsStore,omitempty"` 29 CredentialHelpers map[string]string `json:"credHelpers,omitempty"` 30 Filename string `json:"-"` // Note: for internal use only 31 ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` 32 ServicesFormat string `json:"servicesFormat,omitempty"` 33 TasksFormat string `json:"tasksFormat,omitempty"` 34 SecretFormat string `json:"secretFormat,omitempty"` 35 ConfigFormat string `json:"configFormat,omitempty"` 36 NodesFormat string `json:"nodesFormat,omitempty"` 37 PruneFilters []string `json:"pruneFilters,omitempty"` 38 Proxies map[string]ProxyConfig `json:"proxies,omitempty"` 39 Experimental string `json:"experimental,omitempty"` 40 CurrentContext string `json:"currentContext,omitempty"` 41 CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"` 42 Plugins map[string]map[string]string `json:"plugins,omitempty"` 43 Aliases map[string]string `json:"aliases,omitempty"` 44 } 45 46 // ProxyConfig contains proxy configuration settings 47 type ProxyConfig struct { 48 HTTPProxy string `json:"httpProxy,omitempty"` 49 HTTPSProxy string `json:"httpsProxy,omitempty"` 50 NoProxy string `json:"noProxy,omitempty"` 51 FTPProxy string `json:"ftpProxy,omitempty"` 52 AllProxy string `json:"allProxy,omitempty"` 53 } 54 55 // New initializes an empty configuration file for the given filename 'fn' 56 func New(fn string) *ConfigFile { 57 return &ConfigFile{ 58 AuthConfigs: make(map[string]types.AuthConfig), 59 HTTPHeaders: make(map[string]string), 60 Filename: fn, 61 Plugins: make(map[string]map[string]string), 62 Aliases: make(map[string]string), 63 } 64 } 65 66 // LoadFromReader reads the configuration data given and sets up the auth config 67 // information with given directory and populates the receiver object 68 func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error { 69 if err := json.NewDecoder(configData).Decode(configFile); err != nil && !errors.Is(err, io.EOF) { 70 return err 71 } 72 var err error 73 for addr, ac := range configFile.AuthConfigs { 74 if ac.Auth != "" { 75 ac.Username, ac.Password, err = decodeAuth(ac.Auth) 76 if err != nil { 77 return err 78 } 79 } 80 ac.Auth = "" 81 ac.ServerAddress = addr 82 configFile.AuthConfigs[addr] = ac 83 } 84 return nil 85 } 86 87 // ContainsAuth returns whether there is authentication configured 88 // in this file or not. 89 func (configFile *ConfigFile) ContainsAuth() bool { 90 return configFile.CredentialsStore != "" || 91 len(configFile.CredentialHelpers) > 0 || 92 len(configFile.AuthConfigs) > 0 93 } 94 95 // GetAuthConfigs returns the mapping of repo to auth configuration 96 func (configFile *ConfigFile) GetAuthConfigs() map[string]types.AuthConfig { 97 if configFile.AuthConfigs == nil { 98 configFile.AuthConfigs = make(map[string]types.AuthConfig) 99 } 100 return configFile.AuthConfigs 101 } 102 103 // SaveToWriter encodes and writes out all the authorization information to 104 // the given writer 105 func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { 106 // Encode sensitive data into a new/temp struct 107 tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs)) 108 for k, authConfig := range configFile.AuthConfigs { 109 authCopy := authConfig 110 // encode and save the authstring, while blanking out the original fields 111 authCopy.Auth = encodeAuth(&authCopy) 112 authCopy.Username = "" 113 authCopy.Password = "" 114 authCopy.ServerAddress = "" 115 tmpAuthConfigs[k] = authCopy 116 } 117 118 saveAuthConfigs := configFile.AuthConfigs 119 configFile.AuthConfigs = tmpAuthConfigs 120 defer func() { configFile.AuthConfigs = saveAuthConfigs }() 121 122 // User-Agent header is automatically set, and should not be stored in the configuration 123 for v := range configFile.HTTPHeaders { 124 if strings.EqualFold(v, "User-Agent") { 125 delete(configFile.HTTPHeaders, v) 126 } 127 } 128 129 data, err := json.MarshalIndent(configFile, "", "\t") 130 if err != nil { 131 return err 132 } 133 _, err = writer.Write(data) 134 return err 135 } 136 137 // Save encodes and writes out all the authorization information 138 func (configFile *ConfigFile) Save() (retErr error) { 139 if configFile.Filename == "" { 140 return errors.Errorf("Can't save config with empty filename") 141 } 142 143 dir := filepath.Dir(configFile.Filename) 144 if err := os.MkdirAll(dir, 0o700); err != nil { 145 return err 146 } 147 temp, err := os.CreateTemp(dir, filepath.Base(configFile.Filename)) 148 if err != nil { 149 return err 150 } 151 defer func() { 152 temp.Close() 153 if retErr != nil { 154 if err := os.Remove(temp.Name()); err != nil { 155 logrus.WithError(err).WithField("file", temp.Name()).Debug("Error cleaning up temp file") 156 } 157 } 158 }() 159 160 err = configFile.SaveToWriter(temp) 161 if err != nil { 162 return err 163 } 164 165 if err := temp.Close(); err != nil { 166 return errors.Wrap(err, "error closing temp file") 167 } 168 169 // Handle situation where the configfile is a symlink 170 cfgFile := configFile.Filename 171 if f, err := os.Readlink(cfgFile); err == nil { 172 cfgFile = f 173 } 174 175 // Try copying the current config file (if any) ownership and permissions 176 copyFilePermissions(cfgFile, temp.Name()) 177 return os.Rename(temp.Name(), cfgFile) 178 } 179 180 // ParseProxyConfig computes proxy configuration by retrieving the config for the provided host and 181 // then checking this against any environment variables provided to the container 182 func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts map[string]*string) map[string]*string { 183 var cfgKey string 184 185 if _, ok := configFile.Proxies[host]; !ok { 186 cfgKey = "default" 187 } else { 188 cfgKey = host 189 } 190 191 config := configFile.Proxies[cfgKey] 192 permitted := map[string]*string{ 193 "HTTP_PROXY": &config.HTTPProxy, 194 "HTTPS_PROXY": &config.HTTPSProxy, 195 "NO_PROXY": &config.NoProxy, 196 "FTP_PROXY": &config.FTPProxy, 197 "ALL_PROXY": &config.AllProxy, 198 } 199 m := runOpts 200 if m == nil { 201 m = make(map[string]*string) 202 } 203 for k := range permitted { 204 if *permitted[k] == "" { 205 continue 206 } 207 if _, ok := m[k]; !ok { 208 m[k] = permitted[k] 209 } 210 if _, ok := m[strings.ToLower(k)]; !ok { 211 m[strings.ToLower(k)] = permitted[k] 212 } 213 } 214 return m 215 } 216 217 // encodeAuth creates a base64 encoded string to containing authorization information 218 func encodeAuth(authConfig *types.AuthConfig) string { 219 if authConfig.Username == "" && authConfig.Password == "" { 220 return "" 221 } 222 223 authStr := authConfig.Username + ":" + authConfig.Password 224 msg := []byte(authStr) 225 encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) 226 base64.StdEncoding.Encode(encoded, msg) 227 return string(encoded) 228 } 229 230 // decodeAuth decodes a base64 encoded string and returns username and password 231 func decodeAuth(authStr string) (string, string, error) { 232 if authStr == "" { 233 return "", "", nil 234 } 235 236 decLen := base64.StdEncoding.DecodedLen(len(authStr)) 237 decoded := make([]byte, decLen) 238 authByte := []byte(authStr) 239 n, err := base64.StdEncoding.Decode(decoded, authByte) 240 if err != nil { 241 return "", "", err 242 } 243 if n > decLen { 244 return "", "", errors.Errorf("Something went wrong decoding auth config") 245 } 246 userName, password, ok := strings.Cut(string(decoded), ":") 247 if !ok || userName == "" { 248 return "", "", errors.Errorf("Invalid auth configuration file") 249 } 250 return userName, strings.Trim(password, "\x00"), nil 251 } 252 253 // GetCredentialsStore returns a new credentials store from the settings in the 254 // configuration file 255 func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store { 256 if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" { 257 return newNativeStore(configFile, helper) 258 } 259 return credentials.NewFileStore(configFile) 260 } 261 262 // var for unit testing. 263 var newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store { 264 return credentials.NewNativeStore(configFile, helperSuffix) 265 } 266 267 // GetAuthConfig for a repository from the credential store 268 func (configFile *ConfigFile) GetAuthConfig(registryHostname string) (types.AuthConfig, error) { 269 return configFile.GetCredentialsStore(registryHostname).Get(registryHostname) 270 } 271 272 // getConfiguredCredentialStore returns the credential helper configured for the 273 // given registry, the default credsStore, or the empty string if neither are 274 // configured. 275 func getConfiguredCredentialStore(c *ConfigFile, registryHostname string) string { 276 if c.CredentialHelpers != nil && registryHostname != "" { 277 if helper, exists := c.CredentialHelpers[registryHostname]; exists { 278 return helper 279 } 280 } 281 return c.CredentialsStore 282 } 283 284 // GetAllCredentials returns all of the credentials stored in all of the 285 // configured credential stores. 286 func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) { 287 auths := make(map[string]types.AuthConfig) 288 addAll := func(from map[string]types.AuthConfig) { 289 for reg, ac := range from { 290 auths[reg] = ac 291 } 292 } 293 294 defaultStore := configFile.GetCredentialsStore("") 295 newAuths, err := defaultStore.GetAll() 296 if err != nil { 297 return nil, err 298 } 299 addAll(newAuths) 300 301 // Auth configs from a registry-specific helper should override those from the default store. 302 for registryHostname := range configFile.CredentialHelpers { 303 newAuth, err := configFile.GetAuthConfig(registryHostname) 304 if err != nil { 305 logrus.WithError(err).Warnf("Failed to get credentials for registry: %s", registryHostname) 306 continue 307 } 308 auths[registryHostname] = newAuth 309 } 310 return auths, nil 311 } 312 313 // GetFilename returns the file name that this config file is based on. 314 func (configFile *ConfigFile) GetFilename() string { 315 return configFile.Filename 316 } 317 318 // PluginConfig retrieves the requested option for the given plugin. 319 func (configFile *ConfigFile) PluginConfig(pluginname, option string) (string, bool) { 320 if configFile.Plugins == nil { 321 return "", false 322 } 323 pluginConfig, ok := configFile.Plugins[pluginname] 324 if !ok { 325 return "", false 326 } 327 value, ok := pluginConfig[option] 328 return value, ok 329 } 330 331 // SetPluginConfig sets the option to the given value for the given 332 // plugin. Passing a value of "" will remove the option. If removing 333 // the final config item for a given plugin then also cleans up the 334 // overall plugin entry. 335 func (configFile *ConfigFile) SetPluginConfig(pluginname, option, value string) { 336 if configFile.Plugins == nil { 337 configFile.Plugins = make(map[string]map[string]string) 338 } 339 pluginConfig, ok := configFile.Plugins[pluginname] 340 if !ok { 341 pluginConfig = make(map[string]string) 342 configFile.Plugins[pluginname] = pluginConfig 343 } 344 if value != "" { 345 pluginConfig[option] = value 346 } else { 347 delete(pluginConfig, option) 348 } 349 if len(pluginConfig) == 0 { 350 delete(configFile.Plugins, pluginname) 351 } 352 }