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  }