storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/common-main.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2017-2019 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package cmd
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"encoding/gob"
    24  	"encoding/hex"
    25  	"errors"
    26  	"fmt"
    27  	"math/rand"
    28  	"net"
    29  	"net/url"
    30  	"os"
    31  	"path/filepath"
    32  	"runtime"
    33  	"sort"
    34  	"strings"
    35  	"time"
    36  
    37  	"github.com/fatih/color"
    38  	dns2 "github.com/miekg/dns"
    39  	"github.com/minio/cli"
    40  	"github.com/minio/minio-go/v7/pkg/set"
    41  
    42  	"storj.io/minio/cmd/config"
    43  	"storj.io/minio/cmd/crypto"
    44  	xhttp "storj.io/minio/cmd/http"
    45  	"storj.io/minio/cmd/logger"
    46  	"storj.io/minio/pkg/auth"
    47  	"storj.io/minio/pkg/certs"
    48  	"storj.io/minio/pkg/console"
    49  	"storj.io/minio/pkg/env"
    50  	"storj.io/minio/pkg/handlers"
    51  	"storj.io/minio/pkg/kms"
    52  )
    53  
    54  // serverDebugLog will enable debug printing
    55  var serverDebugLog = env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn
    56  
    57  func init() {
    58  	rand.Seed(time.Now().UTC().UnixNano())
    59  
    60  	logger.Init(GOPATH, GOROOT)
    61  	logger.RegisterError(config.FmtError)
    62  
    63  	// Inject into config package.
    64  	config.Logger.Info = logger.Info
    65  	config.Logger.LogIf = logger.LogIf
    66  
    67  	if IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsKubernetesReplicaSet() || IsPCFTile() {
    68  		// 30 seconds matches the orchestrator DNS TTLs, have
    69  		// a 5 second timeout to lookup from DNS servers.
    70  		globalDNSCache = xhttp.NewDNSCache(30*time.Second, 5*time.Second, logger.LogOnceIf)
    71  	} else {
    72  		// On bare-metals DNS do not change often, so it is
    73  		// safe to assume a higher timeout upto 10 minutes.
    74  		globalDNSCache = xhttp.NewDNSCache(10*time.Minute, 5*time.Second, logger.LogOnceIf)
    75  	}
    76  
    77  	initGlobalContext()
    78  
    79  	globalForwarder = handlers.NewForwarder(&handlers.Forwarder{
    80  		PassHost:     true,
    81  		RoundTripper: newGatewayHTTPTransport(1 * time.Hour),
    82  		Logger: func(err error) {
    83  			if err != nil && !errors.Is(err, context.Canceled) {
    84  				logger.LogIf(GlobalContext, err)
    85  			}
    86  		},
    87  	})
    88  
    89  	globalTransitionState = newTransitionState()
    90  
    91  	console.SetColor("Debug", color.New())
    92  
    93  	gob.Register(StorageErr(""))
    94  }
    95  
    96  func verifyObjectLayerFeatures(name string, objAPI ObjectLayer) {
    97  	if (GlobalKMS != nil) && !objAPI.IsEncryptionSupported() {
    98  		logger.Fatal(errInvalidArgument,
    99  			"Encryption support is requested but '%s' does not support encryption", name)
   100  	}
   101  
   102  	if strings.HasPrefix(name, "gateway") {
   103  		if GlobalGatewaySSE.IsSet() && GlobalKMS == nil {
   104  			uiErr := config.ErrInvalidGWSSEEnvValue(nil).Msg("MINIO_GATEWAY_SSE set but KMS is not configured")
   105  			logger.Fatal(uiErr, "Unable to start gateway with SSE")
   106  		}
   107  	}
   108  
   109  	globalCompressConfigMu.Lock()
   110  	if globalCompressConfig.Enabled && !objAPI.IsCompressionSupported() {
   111  		logger.Fatal(errInvalidArgument,
   112  			"Compression support is requested but '%s' does not support compression", name)
   113  	}
   114  	globalCompressConfigMu.Unlock()
   115  }
   116  
   117  // Check for updates and print a notification message
   118  func checkUpdate(mode string) {
   119  	updateURL := minioReleaseInfoURL
   120  	if runtime.GOOS == globalWindowsOSName {
   121  		updateURL = minioReleaseWindowsInfoURL
   122  	}
   123  
   124  	u, err := url.Parse(updateURL)
   125  	if err != nil {
   126  		return
   127  	}
   128  
   129  	// Its OK to ignore any errors during doUpdate() here.
   130  	crTime, err := GetCurrentReleaseTime()
   131  	if err != nil {
   132  		return
   133  	}
   134  
   135  	_, lrTime, err := getLatestReleaseTime(u, 2*time.Second, mode)
   136  	if err != nil {
   137  		return
   138  	}
   139  
   140  	var older time.Duration
   141  	var downloadURL string
   142  	if lrTime.After(crTime) {
   143  		older = lrTime.Sub(crTime)
   144  		downloadURL = getDownloadURL(releaseTimeToReleaseTag(lrTime))
   145  	}
   146  
   147  	updateMsg := prepareUpdateMessage(downloadURL, older)
   148  	if updateMsg == "" {
   149  		return
   150  	}
   151  
   152  	logStartupMessage(prepareUpdateMessage("Run `mc admin update`", lrTime.Sub(crTime)))
   153  }
   154  
   155  func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
   156  	var dir string
   157  	var dirSet bool
   158  
   159  	switch {
   160  	case ctx.IsSet(option):
   161  		dir = ctx.String(option)
   162  		dirSet = true
   163  	case ctx.GlobalIsSet(option):
   164  		dir = ctx.GlobalString(option)
   165  		dirSet = true
   166  		// cli package does not expose parent's option option.  Below code is workaround.
   167  		if dir == "" || dir == getDefaultDir() {
   168  			dirSet = false // Unset to false since GlobalIsSet() true is a false positive.
   169  			if ctx.Parent().GlobalIsSet(option) {
   170  				dir = ctx.Parent().GlobalString(option)
   171  				dirSet = true
   172  			}
   173  		}
   174  	default:
   175  		// Neither local nor global option is provided.  In this case, try to use
   176  		// default directory.
   177  		dir = getDefaultDir()
   178  		if dir == "" {
   179  			logger.FatalIf(errInvalidArgument, "%s option must be provided", option)
   180  		}
   181  	}
   182  
   183  	if dir == "" {
   184  		logger.FatalIf(errors.New("empty directory"), "%s directory cannot be empty", option)
   185  	}
   186  
   187  	// Disallow relative paths, figure out absolute paths.
   188  	dirAbs, err := filepath.Abs(dir)
   189  	logger.FatalIf(err, "Unable to fetch absolute path for %s=%s", option, dir)
   190  
   191  	logger.FatalIf(mkdirAllIgnorePerm(dirAbs), "Unable to create directory specified %s=%s", option, dir)
   192  
   193  	return &ConfigDir{path: dirAbs}, dirSet
   194  }
   195  
   196  func handleCommonCmdArgs(ctx *cli.Context) {
   197  
   198  	// Get "json" flag from command line argument and
   199  	// enable json and quite modes if json flag is turned on.
   200  	GlobalCLIContext.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json")
   201  	if GlobalCLIContext.JSON {
   202  		logger.EnableJSON()
   203  	}
   204  
   205  	// Get quiet flag from command line argument.
   206  	GlobalCLIContext.Quiet = ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
   207  	if GlobalCLIContext.Quiet {
   208  		logger.EnableQuiet()
   209  	}
   210  
   211  	// Get anonymous flag from command line argument.
   212  	GlobalCLIContext.Anonymous = ctx.IsSet("anonymous") || ctx.GlobalIsSet("anonymous")
   213  	if GlobalCLIContext.Anonymous {
   214  		logger.EnableAnonymous()
   215  	}
   216  
   217  	// Fetch address option
   218  	GlobalCLIContext.Addr = ctx.GlobalString("address")
   219  	if GlobalCLIContext.Addr == "" || GlobalCLIContext.Addr == ":"+GlobalMinioDefaultPort {
   220  		GlobalCLIContext.Addr = ctx.String("address")
   221  	}
   222  
   223  	// Check "no-compat" flag from command line argument.
   224  	GlobalCLIContext.StrictS3Compat = true
   225  	if ctx.IsSet("no-compat") || ctx.GlobalIsSet("no-compat") {
   226  		GlobalCLIContext.StrictS3Compat = false
   227  	}
   228  
   229  	// Set all config, certs and CAs directories.
   230  	var configSet, certsSet bool
   231  	globalConfigDir, configSet = newConfigDirFromCtx(ctx, "config-dir", defaultConfigDir.Get)
   232  	globalCertsDir, certsSet = newConfigDirFromCtx(ctx, "certs-dir", defaultCertsDir.Get)
   233  
   234  	// Remove this code when we deprecate and remove config-dir.
   235  	// This code is to make sure we inherit from the config-dir
   236  	// option if certs-dir is not provided.
   237  	if !certsSet && configSet {
   238  		globalCertsDir = &ConfigDir{path: filepath.Join(globalConfigDir.Get(), certsDir)}
   239  	}
   240  
   241  	globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}
   242  
   243  	logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
   244  }
   245  
   246  func HandleCommonEnvVars() {
   247  	wormEnabled, err := config.LookupWorm()
   248  	if err != nil {
   249  		logger.Fatal(config.ErrInvalidWormValue(err), "Invalid worm configuration")
   250  	}
   251  	if wormEnabled {
   252  		logger.Fatal(errors.New("WORM is deprecated"), "global MINIO_WORM support is removed, please downgrade your server or migrate to https://storj.io/minio/tree/master/docs/retention")
   253  	}
   254  
   255  	globalBrowserEnabled, err = config.ParseBool(env.Get(config.EnvBrowser, config.EnableOn))
   256  	if err != nil {
   257  		logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
   258  	}
   259  
   260  	globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
   261  	if err != nil {
   262  		logger.Fatal(config.ErrInvalidFSOSyncValue(err), "Invalid MINIO_FS_OSYNC value in environment variable")
   263  	}
   264  
   265  	domains := env.Get(config.EnvDomain, "")
   266  	if len(domains) != 0 {
   267  		for _, domainName := range strings.Split(domains, config.ValueSeparator) {
   268  			if _, ok := dns2.IsDomainName(domainName); !ok {
   269  				logger.Fatal(config.ErrInvalidDomainValue(nil).Msg("Unknown value `%s`", domainName),
   270  					"Invalid MINIO_DOMAIN value in environment variable")
   271  			}
   272  			globalDomainNames = append(globalDomainNames, domainName)
   273  		}
   274  		sort.Strings(globalDomainNames)
   275  		lcpSuf := lcpSuffix(globalDomainNames)
   276  		for _, domainName := range globalDomainNames {
   277  			if domainName == lcpSuf && len(globalDomainNames) > 1 {
   278  				logger.Fatal(config.ErrOverlappingDomainValue(nil).Msg("Overlapping domains `%s` not allowed", globalDomainNames),
   279  					"Invalid MINIO_DOMAIN value in environment variable")
   280  			}
   281  		}
   282  	}
   283  
   284  	publicIPs := env.Get(config.EnvPublicIPs, "")
   285  	if len(publicIPs) != 0 {
   286  		minioEndpoints := strings.Split(publicIPs, config.ValueSeparator)
   287  		var domainIPs = set.NewStringSet()
   288  		for _, endpoint := range minioEndpoints {
   289  			if net.ParseIP(endpoint) == nil {
   290  				// Checking if the IP is a DNS entry.
   291  				addrs, err := net.LookupHost(endpoint)
   292  				if err != nil {
   293  					logger.FatalIf(err, "Unable to initialize MinIO server with [%s] invalid entry found in MINIO_PUBLIC_IPS", endpoint)
   294  				}
   295  				for _, addr := range addrs {
   296  					domainIPs.Add(addr)
   297  				}
   298  			}
   299  			domainIPs.Add(endpoint)
   300  		}
   301  		updateDomainIPs(domainIPs)
   302  	} else {
   303  		// Add found interfaces IP address to global domain IPS,
   304  		// loopback addresses will be naturally dropped.
   305  		domainIPs := mustGetLocalIP4()
   306  		for _, host := range globalEndpoints.Hostnames() {
   307  			domainIPs.Add(host)
   308  		}
   309  		updateDomainIPs(domainIPs)
   310  	}
   311  
   312  	// In place update is true by default if the MINIO_UPDATE is not set
   313  	// or is not set to 'off', if MINIO_UPDATE is set to 'off' then
   314  	// in-place update is off.
   315  	globalInplaceUpdateDisabled = strings.EqualFold(env.Get(config.EnvUpdate, config.EnableOn), config.EnableOff)
   316  
   317  	if env.IsSet(config.EnvAccessKey) || env.IsSet(config.EnvSecretKey) {
   318  		cred, err := auth.CreateCredentials(env.Get(config.EnvAccessKey, ""), env.Get(config.EnvSecretKey, ""))
   319  		if err != nil {
   320  			logger.Fatal(config.ErrInvalidCredentials(err),
   321  				"Unable to validate credentials inherited from the shell environment")
   322  		}
   323  		globalActiveCred = cred
   324  	}
   325  
   326  	if env.IsSet(config.EnvRootUser) || env.IsSet(config.EnvRootPassword) {
   327  		cred, err := auth.CreateCredentials(env.Get(config.EnvRootUser, ""), env.Get(config.EnvRootPassword, ""))
   328  		if err != nil {
   329  			logger.Fatal(config.ErrInvalidCredentials(err),
   330  				"Unable to validate credentials inherited from the shell environment")
   331  		}
   332  		globalActiveCred = cred
   333  	}
   334  
   335  	if env.IsSet(config.EnvKMSSecretKey) && env.IsSet(config.EnvKESEndpoint) {
   336  		logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", config.EnvKMSSecretKey, config.EnvKESEndpoint))
   337  	}
   338  	switch {
   339  	case env.IsSet(config.EnvKMSSecretKey) && env.IsSet(config.EnvKESEndpoint):
   340  		logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", config.EnvKMSSecretKey, config.EnvKESEndpoint))
   341  	case env.IsSet(config.EnvKMSMasterKey) && env.IsSet(config.EnvKESEndpoint):
   342  		logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", config.EnvKMSMasterKey, config.EnvKESEndpoint))
   343  	}
   344  	if env.IsSet(config.EnvKMSSecretKey) {
   345  		KMS, err := kms.Parse(env.Get(config.EnvKMSSecretKey, ""))
   346  		if err != nil {
   347  			logger.Fatal(err, "Unable to parse the KMS secret key inherited from the shell environment")
   348  		}
   349  		GlobalKMS = KMS
   350  	} else if env.IsSet(config.EnvKMSMasterKey) {
   351  		logger.LogIf(GlobalContext, errors.New("legacy KMS configuration"), fmt.Sprintf("The environment variable %q is deprecated and will be removed in the future", config.EnvKMSMasterKey))
   352  
   353  		v := strings.SplitN(env.Get(config.EnvKMSMasterKey, ""), ":", 2)
   354  		if len(v) != 2 {
   355  			logger.Fatal(errors.New("invalid "+config.EnvKMSMasterKey), "Unable to parse the KMS secret key inherited from the shell environment")
   356  		}
   357  		secretKey, err := hex.DecodeString(v[1])
   358  		if err != nil {
   359  			logger.Fatal(err, "Unable to parse the KMS secret key inherited from the shell environment")
   360  		}
   361  		KMS, err := kms.New(v[0], secretKey)
   362  		if err != nil {
   363  			logger.Fatal(err, "Unable to parse the KMS secret key inherited from the shell environment")
   364  		}
   365  		GlobalKMS = KMS
   366  	}
   367  	if env.IsSet(config.EnvKESEndpoint) {
   368  		kesEndpoints, err := crypto.ParseKESEndpoints(env.Get(config.EnvKESEndpoint, ""))
   369  		if err != nil {
   370  			logger.Fatal(err, "Unable to parse the KES endpoints inherited from the shell environment")
   371  		}
   372  		KMS, err := crypto.NewKes(crypto.KesConfig{
   373  			Enabled:      true,
   374  			Endpoint:     kesEndpoints,
   375  			DefaultKeyID: env.Get(config.EnvKESKeyName, ""),
   376  			CertFile:     env.Get(config.EnvKESClientCert, ""),
   377  			KeyFile:      env.Get(config.EnvKESClientKey, ""),
   378  			CAPath:       env.Get(config.EnvKESServerCA, globalCertsCADir.Get()),
   379  			Transport:    newCustomHTTPTransportWithHTTP2(&tls.Config{RootCAs: globalRootCAs}, defaultDialTimeout)(),
   380  		})
   381  		if err != nil {
   382  			logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
   383  		}
   384  		GlobalKMS = KMS
   385  	}
   386  }
   387  
   388  func logStartupMessage(msg string) {
   389  	if globalConsoleSys != nil {
   390  		globalConsoleSys.Send(msg, string(logger.All))
   391  	}
   392  	logger.StartupMessage(msg)
   393  }
   394  
   395  func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
   396  	if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
   397  		return nil, nil, false, nil
   398  	}
   399  
   400  	if x509Certs, err = config.ParsePublicCertFile(getPublicCertFile()); err != nil {
   401  		return nil, nil, false, err
   402  	}
   403  
   404  	manager, err = certs.NewManager(GlobalContext, getPublicCertFile(), getPrivateKeyFile(), config.LoadX509KeyPair)
   405  	if err != nil {
   406  		return nil, nil, false, err
   407  	}
   408  
   409  	// MinIO has support for multiple certificates. It expects the following structure:
   410  	//  certs/
   411  	//   │
   412  	//   ├─ public.crt
   413  	//   ├─ private.key
   414  	//   │
   415  	//   ├─ example.com/
   416  	//   │   │
   417  	//   │   ├─ public.crt
   418  	//   │   └─ private.key
   419  	//   └─ foobar.org/
   420  	//      │
   421  	//      ├─ public.crt
   422  	//      └─ private.key
   423  	//   ...
   424  	//
   425  	// Therefore, we read all filenames in the cert directory and check
   426  	// for each directory whether it contains a public.crt and private.key.
   427  	// If so, we try to add it to certificate manager.
   428  	root, err := os.Open(globalCertsDir.Get())
   429  	if err != nil {
   430  		return nil, nil, false, err
   431  	}
   432  	defer root.Close()
   433  
   434  	files, err := root.Readdir(-1)
   435  	if err != nil {
   436  		return nil, nil, false, err
   437  	}
   438  	for _, file := range files {
   439  		// Ignore all
   440  		// - regular files
   441  		// - "CAs" directory
   442  		// - any directory which starts with ".."
   443  		if file.Mode().IsRegular() || file.Name() == "CAs" || strings.HasPrefix(file.Name(), "..") {
   444  			continue
   445  		}
   446  		if file.Mode()&os.ModeSymlink == os.ModeSymlink {
   447  			file, err = os.Stat(filepath.Join(root.Name(), file.Name()))
   448  			if err != nil {
   449  				// not accessible ignore
   450  				continue
   451  			}
   452  			if !file.IsDir() {
   453  				continue
   454  			}
   455  		}
   456  
   457  		var (
   458  			certFile = filepath.Join(root.Name(), file.Name(), publicCertFile)
   459  			keyFile  = filepath.Join(root.Name(), file.Name(), privateKeyFile)
   460  		)
   461  		if !isFile(certFile) || !isFile(keyFile) {
   462  			continue
   463  		}
   464  		if err = manager.AddCertificate(certFile, keyFile); err != nil {
   465  			err = fmt.Errorf("Unable to load TLS certificate '%s,%s': %w", certFile, keyFile, err)
   466  			logger.LogIf(GlobalContext, err, logger.Minio)
   467  		}
   468  	}
   469  	secureConn = true
   470  	return x509Certs, manager, secureConn, nil
   471  }
   472  
   473  // contextCanceled returns whether a context is canceled.
   474  func contextCanceled(ctx context.Context) bool {
   475  	select {
   476  	case <-ctx.Done():
   477  		return true
   478  	default:
   479  		return false
   480  	}
   481  }