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