github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/config.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "bufio" 22 "fmt" 23 "net/url" 24 "os" 25 "path/filepath" 26 "regexp" 27 "runtime" 28 "strings" 29 30 "github.com/minio/mc/pkg/probe" 31 "github.com/minio/pkg/v2/env" 32 33 "github.com/mitchellh/go-homedir" 34 ) 35 36 // mcCustomConfigDir contains the whole path to config dir. Only access via get/set functions. 37 var mcCustomConfigDir string 38 39 // setMcConfigDir - set a custom MinIO Client config folder. 40 func setMcConfigDir(configDir string) { 41 mcCustomConfigDir = configDir 42 } 43 44 // getMcConfigDir - construct MinIO Client config folder. 45 func getMcConfigDir() (string, *probe.Error) { 46 if mcCustomConfigDir != "" { 47 return mcCustomConfigDir, nil 48 } 49 homeDir, e := homedir.Dir() 50 if e != nil { 51 return "", probe.NewError(e) 52 } 53 configDir := filepath.Join(homeDir, defaultMCConfigDir()) 54 return configDir, nil 55 } 56 57 // Return default default mc config directory. 58 // Generally you want to use getMcConfigDir which returns custom overrides. 59 func defaultMCConfigDir() string { 60 if runtime.GOOS == "windows" { 61 // For windows the path is slightly different 62 cmd := filepath.Base(os.Args[0]) 63 if strings.HasSuffix(strings.ToLower(cmd), ".exe") { 64 cmd = cmd[:strings.LastIndex(cmd, ".")] 65 } 66 return fmt.Sprintf("%s\\", cmd) 67 } 68 return fmt.Sprintf(".%s/", filepath.Base(os.Args[0])) 69 } 70 71 // mustGetMcConfigDir - construct MinIO Client config folder or fail 72 func mustGetMcConfigDir() (configDir string) { 73 configDir, err := getMcConfigDir() 74 fatalIf(err.Trace(), "Unable to get mcConfigDir.") 75 76 return configDir 77 } 78 79 // createMcConfigDir - create MinIO Client config folder 80 func createMcConfigDir() *probe.Error { 81 p, err := getMcConfigDir() 82 if err != nil { 83 return err.Trace() 84 } 85 if e := os.MkdirAll(p, 0o700); e != nil { 86 return probe.NewError(e) 87 } 88 return nil 89 } 90 91 // getMcConfigPath - construct MinIO Client configuration path 92 func getMcConfigPath() (string, *probe.Error) { 93 if mcCustomConfigDir != "" { 94 return filepath.Join(mcCustomConfigDir, globalMCConfigFile), nil 95 } 96 dir, err := getMcConfigDir() 97 if err != nil { 98 return "", err.Trace() 99 } 100 return filepath.Join(dir, globalMCConfigFile), nil 101 } 102 103 // mustGetMcConfigPath - similar to getMcConfigPath, ignores errors 104 func mustGetMcConfigPath() string { 105 path, err := getMcConfigPath() 106 fatalIf(err.Trace(), "Unable to get mcConfigPath.") 107 108 return path 109 } 110 111 // newMcConfig - initializes a new version '10' config. 112 func newMcConfig() *configV10 { 113 cfg := newConfigV10() 114 cfg.loadDefaults() 115 return cfg 116 } 117 118 // loadMcConfigCached - returns loadMcConfig with a closure for config cache. 119 func loadMcConfigFactory() func() (*configV10, *probe.Error) { 120 // Load once and cache in a closure. 121 cfgCache, err := loadConfigV10() 122 123 // loadMcConfig - reads configuration file and returns config. 124 return func() (*configV10, *probe.Error) { 125 return cfgCache, err 126 } 127 } 128 129 // loadMcConfig - returns configuration, initialized later. 130 var loadMcConfig func() (*configV10, *probe.Error) 131 132 // saveMcConfig - saves configuration file and returns error if any. 133 func saveMcConfig(config *configV10) *probe.Error { 134 if config == nil { 135 return errInvalidArgument().Trace() 136 } 137 138 err := createMcConfigDir() 139 if err != nil { 140 return err.Trace(mustGetMcConfigDir()) 141 } 142 143 // Save the config. 144 if err := saveConfigV10(config); err != nil { 145 return err.Trace(mustGetMcConfigPath()) 146 } 147 148 // Refresh the config cache. 149 loadMcConfig = loadMcConfigFactory() 150 return nil 151 } 152 153 // isMcConfigExists returns err if config doesn't exist. 154 func isMcConfigExists() bool { 155 configFile, err := getMcConfigPath() 156 if err != nil { 157 return false 158 } 159 if _, e := os.Stat(configFile); e != nil { 160 return false 161 } 162 return true 163 } 164 165 // cleanAlias removes any forbidden trailing slashes or backslashes 166 // before any validation to avoid annoying mc complaints. 167 func cleanAlias(s string) string { 168 s = strings.TrimSuffix(s, "/") 169 s = strings.TrimSuffix(s, "\\") 170 return s 171 } 172 173 // isValidAlias - Check if alias valid. 174 func isValidAlias(alias string) bool { 175 return regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9-_]*$").MatchString(alias) 176 } 177 178 // getAliasConfig retrieves host specific configuration such as access keys, signature type. 179 func getAliasConfig(alias string) (*aliasConfigV10, *probe.Error) { 180 mcCfg, err := loadMcConfig() 181 if err != nil { 182 return nil, err.Trace(alias) 183 } 184 185 // if host is exact return quickly. 186 if _, ok := mcCfg.Aliases[alias]; ok { 187 hostCfg := mcCfg.Aliases[alias] 188 return &hostCfg, nil 189 } 190 191 // return error if cannot be matched. 192 return nil, errNoMatchingHost(alias).Trace(alias) 193 } 194 195 // mustGetHostConfig retrieves host specific configuration such as access keys, signature type. 196 func mustGetHostConfig(alias string) *aliasConfigV10 { 197 aliasCfg, _ := getAliasConfig(alias) 198 // If alias is not found, 199 // look for it in the environment variable. 200 if aliasCfg == nil { 201 aliasCfg, _ = expandAliasFromEnv(env.Get(mcEnvHostPrefix+alias, "")) 202 } 203 if aliasCfg == nil { 204 aliasCfg = aliasToConfigMap[alias] 205 } 206 return aliasCfg 207 } 208 209 var ( 210 hostKeys = regexp.MustCompile("^(https?://)(.*?):(.*)@(.*?)$") 211 hostKeyTokens = regexp.MustCompile("^(https?://)(.*?):(.*?):(.*)@(.*?)$") 212 ) 213 214 // parse url usually obtained from env. 215 func parseEnvURLStr(envURL string) (*url.URL, string, string, string, *probe.Error) { 216 var accessKey, secretKey, sessionToken string 217 var parsedURL string 218 if hostKeyTokens.MatchString(envURL) { 219 parts := hostKeyTokens.FindStringSubmatch(envURL) 220 if len(parts) != 6 { 221 return nil, "", "", "", errInvalidArgument().Trace(envURL) 222 } 223 accessKey = parts[2] 224 secretKey = parts[3] 225 sessionToken = parts[4] 226 parsedURL = fmt.Sprintf("%s%s", parts[1], parts[5]) 227 } else if hostKeys.MatchString(envURL) { 228 parts := hostKeys.FindStringSubmatch(envURL) 229 if len(parts) != 5 { 230 return nil, "", "", "", errInvalidArgument().Trace(envURL) 231 } 232 accessKey = parts[2] 233 secretKey = parts[3] 234 parsedURL = fmt.Sprintf("%s%s", parts[1], parts[4]) 235 } 236 var u *url.URL 237 var e error 238 if parsedURL != "" { 239 u, e = url.Parse(parsedURL) 240 } else { 241 u, e = url.Parse(envURL) 242 } 243 if e != nil { 244 return nil, "", "", "", probe.NewError(e) 245 } 246 // Look for if URL has invalid values and return error. 247 if !((u.Scheme == "http" || u.Scheme == "https") && 248 (u.Path == "/" || u.Path == "") && u.Opaque == "" && 249 !u.ForceQuery && u.RawQuery == "" && u.Fragment == "") { 250 return nil, "", "", "", errInvalidArgument().Trace(u.String()) 251 } 252 if accessKey == "" && secretKey == "" { 253 if u.User != nil { 254 accessKey = u.User.Username() 255 secretKey, _ = u.User.Password() 256 } 257 } 258 return u, accessKey, secretKey, sessionToken, nil 259 } 260 261 const ( 262 mcEnvHostPrefix = "MC_HOST_" 263 mcEnvConfigFile = "MC_CONFIG_ENV_FILE" 264 ) 265 266 var aliasToConfigMap = make(map[string]*aliasConfigV10) 267 268 func readAliasesFromFile(envConfigFile string) *probe.Error { 269 r, e := os.Open(envConfigFile) 270 if e != nil { 271 return probe.NewError(e).Trace(envConfigFile) 272 } 273 defer r.Close() 274 scanner := bufio.NewScanner(r) 275 for scanner.Scan() { 276 envLine := scanner.Text() 277 strs := strings.SplitN(envLine, "=", 2) 278 if len(strs) != 2 { 279 return probe.NewError(fmt.Errorf("parsing error at %s", envLine)).Trace(envConfigFile) 280 } 281 alias := strings.TrimPrefix(strs[0], mcEnvHostPrefix) 282 if len(alias) == 0 { 283 return probe.NewError(fmt.Errorf("parsing error at %s", envLine)).Trace(envConfigFile) 284 } 285 aliasConfig, err := expandAliasFromEnv(strs[1]) 286 if err != nil { 287 return err.Trace(envLine) 288 } 289 aliasToConfigMap[alias] = aliasConfig 290 } 291 if e := scanner.Err(); e != nil { 292 return probe.NewError(e).Trace(envConfigFile) 293 } 294 return nil 295 } 296 297 func expandAliasFromEnv(envURL string) (*aliasConfigV10, *probe.Error) { 298 u, accessKey, secretKey, sessionToken, err := parseEnvURLStr(envURL) 299 if err != nil { 300 return nil, err.Trace(envURL) 301 } 302 303 return &aliasConfigV10{ 304 URL: u.String(), 305 API: "S3v4", 306 AccessKey: accessKey, 307 SecretKey: secretKey, 308 SessionToken: sessionToken, 309 }, nil 310 } 311 312 // expandAlias expands aliased URL if any match is found, returns as is otherwise. 313 func expandAlias(aliasedURL string) (alias, urlStr string, aliasCfg *aliasConfigV10, err *probe.Error) { 314 // Extract alias from the URL. 315 alias, path := url2Alias(aliasedURL) 316 317 if env.IsSet(mcEnvHostPrefix + alias) { 318 aliasCfg, err = expandAliasFromEnv(env.Get(mcEnvHostPrefix+alias, "")) 319 if err != nil { 320 return "", "", nil, err.Trace(aliasedURL) 321 } 322 return alias, urlJoinPath(aliasCfg.URL, path), aliasCfg, nil 323 } 324 325 aliasCfg = aliasToConfigMap[alias] 326 if aliasCfg != nil { 327 return alias, urlJoinPath(aliasCfg.URL, path), aliasCfg, nil 328 } 329 330 // Find the matching alias entry and expand the URL. 331 if aliasCfg = mustGetHostConfig(alias); aliasCfg != nil { 332 return alias, urlJoinPath(aliasCfg.URL, path), aliasCfg, nil 333 } 334 335 return "", aliasedURL, nil, nil // No matching entry found. Return original URL as is. 336 } 337 338 // mustExpandAlias expands aliased URL if any match is found, returns as is otherwise. 339 func mustExpandAlias(aliasedURL string) (alias, urlStr string, aliasCfg *aliasConfigV10) { 340 alias, urlStr, aliasCfg, _ = expandAlias(aliasedURL) 341 return alias, urlStr, aliasCfg 342 }