// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package cmd

import (
	"bufio"
	"bytes"
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/gob"
	"encoding/pem"
	"errors"
	"fmt"
	"math/rand"
	"net"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/dustin/go-humanize"
	fcolor "github.com/fatih/color"
	"github.com/go-openapi/loads"
	"github.com/inconshreveable/mousetrap"
	dns2 "github.com/miekg/dns"
	"github.com/minio/cli"
	consoleapi "github.com/minio/console/api"
	"github.com/minio/console/api/operations"
	consoleoauth2 "github.com/minio/console/pkg/auth/idp/oauth2"
	consoleCerts "github.com/minio/console/pkg/certs"
	"github.com/minio/kes-go"
	"github.com/minio/madmin-go/v3"
	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/set"
	"github.com/minio/minio/internal/auth"
	"github.com/minio/minio/internal/color"
	"github.com/minio/minio/internal/config"
	"github.com/minio/minio/internal/kms"
	"github.com/minio/minio/internal/logger"
	"github.com/minio/pkg/v2/certs"
	"github.com/minio/pkg/v2/console"
	"github.com/minio/pkg/v2/ellipses"
	"github.com/minio/pkg/v2/env"
	xnet "github.com/minio/pkg/v2/net"
)

// serverDebugLog will enable debug printing
var serverDebugLog = env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn

var currentReleaseTime time.Time

func init() {
	if runtime.GOOS == "windows" {
		if mousetrap.StartedByExplorer() {
			fmt.Printf("Don't double-click %s\n", os.Args[0])
			fmt.Println("You need to open cmd.exe/PowerShell and run it from the command line")
			fmt.Println("Refer to the docs here on how to run it as a Windows Service https://github.com/minio/minio-service/tree/master/windows")
			fmt.Println("Press the Enter Key to Exit")
			fmt.Scanln()
			os.Exit(1)
		}
	}

	rand.Seed(time.Now().UTC().UnixNano())

	logger.Init(GOPATH, GOROOT)
	logger.RegisterError(config.FmtError)

	globalBatchJobsMetrics = batchJobMetrics{metrics: make(map[string]*batchJobInfo)}
	go globalBatchJobsMetrics.purgeJobMetrics()

	t, _ := minioVersionToReleaseTime(Version)
	if !t.IsZero() {
		globalVersionUnix = uint64(t.Unix())
	}

	globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != ""

	console.SetColor("Debug", fcolor.New())

	gob.Register(StorageErr(""))
	gob.Register(madmin.TimeInfo{})
	gob.Register(madmin.XFSErrorConfigs{})
	gob.Register(map[string]string{})
	gob.Register(map[string]interface{}{})

	// All minio-go and madmin-go API operations shall be performed only once,
	// another way to look at this is we are turning off retries.
	minio.MaxRetry = 1
	madmin.MaxRetry = 1

	currentReleaseTime, _ = GetCurrentReleaseTime()
}

const consolePrefix = "CONSOLE_"

func minioConfigToConsoleFeatures() {
	os.Setenv("CONSOLE_PBKDF_SALT", globalDeploymentID())
	os.Setenv("CONSOLE_PBKDF_PASSPHRASE", globalDeploymentID())
	if globalMinioEndpoint != "" {
		os.Setenv("CONSOLE_MINIO_SERVER", globalMinioEndpoint)
	} else {
		// Explicitly set 127.0.0.1 so Console will automatically bypass TLS verification to the local S3 API.
		// This will save users from providing a certificate with IP or FQDN SAN that points to the local host.
		os.Setenv("CONSOLE_MINIO_SERVER", fmt.Sprintf("%s://127.0.0.1:%s", getURLScheme(globalIsTLS), globalMinioPort))
	}
	if value := env.Get(config.EnvMinIOLogQueryURL, ""); value != "" {
		os.Setenv("CONSOLE_LOG_QUERY_URL", value)
		if value := env.Get(config.EnvMinIOLogQueryAuthToken, ""); value != "" {
			os.Setenv("CONSOLE_LOG_QUERY_AUTH_TOKEN", value)
		}
	}
	// pass the console subpath configuration
	if globalBrowserRedirectURL != nil {
		subPath := path.Clean(pathJoin(strings.TrimSpace(globalBrowserRedirectURL.Path), SlashSeparator))
		if subPath != SlashSeparator {
			os.Setenv("CONSOLE_SUBPATH", subPath)
		}
	}
	// Enable if prometheus URL is set.
	if value := env.Get(config.EnvMinIOPrometheusURL, ""); value != "" {
		os.Setenv("CONSOLE_PROMETHEUS_URL", value)
		if value := env.Get(config.EnvMinIOPrometheusJobID, "minio-job"); value != "" {
			os.Setenv("CONSOLE_PROMETHEUS_JOB_ID", value)
			// Support additional labels for more granular filtering.
			if value := env.Get(config.EnvMinIOPrometheusExtraLabels, ""); value != "" {
				os.Setenv("CONSOLE_PROMETHEUS_EXTRA_LABELS", value)
			}
		}
		// Support Prometheus Auth Token
		if value := env.Get(config.EnvMinIOPrometheusAuthToken, ""); value != "" {
			os.Setenv("CONSOLE_PROMETHEUS_AUTH_TOKEN", value)
		}
	}
	// Enable if LDAP is enabled.
	if globalIAMSys.LDAPConfig.Enabled() {
		os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn)
	}
	// Handle animation in welcome page
	if value := env.Get(config.EnvBrowserLoginAnimation, "on"); value != "" {
		os.Setenv("CONSOLE_ANIMATED_LOGIN", value)
	}

	// Pass on the session duration environment variable, else we will default to 12 hours
	if valueSts := env.Get(config.EnvMinioStsDuration, ""); valueSts != "" {
		os.Setenv("CONSOLE_STS_DURATION", valueSts)
	} else if valueSession := env.Get(config.EnvBrowserSessionDuration, ""); valueSession != "" {
		os.Setenv("CONSOLE_STS_DURATION", valueSession)
	}

	os.Setenv("CONSOLE_MINIO_REGION", globalSite.Region)
	os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", ""))

	// This section sets Browser (console) stored config
	if valueSCP := globalBrowserConfig.GetCSPolicy(); valueSCP != "" {
		os.Setenv("CONSOLE_SECURE_CONTENT_SECURITY_POLICY", valueSCP)
	}

	if hstsSeconds := globalBrowserConfig.GetHSTSSeconds(); hstsSeconds > 0 {
		isubdom := globalBrowserConfig.IsHSTSIncludeSubdomains()
		isprel := globalBrowserConfig.IsHSTSPreload()
		os.Setenv("CONSOLE_SECURE_STS_SECONDS", strconv.Itoa(hstsSeconds))
		os.Setenv("CONSOLE_SECURE_STS_INCLUDE_SUB_DOMAINS", isubdom)
		os.Setenv("CONSOLE_SECURE_STS_PRELOAD", isprel)
	}

	if valueRefer := globalBrowserConfig.GetReferPolicy(); valueRefer != "" {
		os.Setenv("CONSOLE_SECURE_REFERRER_POLICY", valueRefer)
	}

	globalSubnetConfig.ApplyEnv()
}

func buildOpenIDConsoleConfig() consoleoauth2.OpenIDPCfg {
	pcfgs := globalIAMSys.OpenIDConfig.ProviderCfgs
	m := make(map[string]consoleoauth2.ProviderConfig, len(pcfgs))
	for name, cfg := range pcfgs {
		callback := getConsoleEndpoints()[0] + "/oauth_callback"
		if cfg.RedirectURI != "" {
			callback = cfg.RedirectURI
		}
		m[name] = consoleoauth2.ProviderConfig{
			URL:                     cfg.URL.String(),
			DisplayName:             cfg.DisplayName,
			ClientID:                cfg.ClientID,
			ClientSecret:            cfg.ClientSecret,
			HMACSalt:                globalDeploymentID(),
			HMACPassphrase:          cfg.ClientID,
			Scopes:                  strings.Join(cfg.DiscoveryDoc.ScopesSupported, ","),
			Userinfo:                cfg.ClaimUserinfo,
			RedirectCallbackDynamic: cfg.RedirectURIDynamic,
			RedirectCallback:        callback,
			EndSessionEndpoint:      cfg.DiscoveryDoc.EndSessionEndpoint,
			RoleArn:                 cfg.GetRoleArn(),
		}
	}
	return m
}

func initConsoleServer() (*consoleapi.Server, error) {
	// unset all console_ environment variables.
	for _, cenv := range env.List(consolePrefix) {
		os.Unsetenv(cenv)
	}

	// enable all console environment variables
	minioConfigToConsoleFeatures()

	// set certs dir to minio directory
	consoleCerts.GlobalCertsDir = &consoleCerts.ConfigDir{
		Path: globalCertsDir.Get(),
	}
	consoleCerts.GlobalCertsCADir = &consoleCerts.ConfigDir{
		Path: globalCertsCADir.Get(),
	}

	// set certs before other console initialization
	consoleapi.GlobalRootCAs, consoleapi.GlobalPublicCerts, consoleapi.GlobalTLSCertsManager = globalRootCAs, globalPublicCerts, globalTLSCerts

	swaggerSpec, err := loads.Embedded(consoleapi.SwaggerJSON, consoleapi.FlatSwaggerJSON)
	if err != nil {
		return nil, err
	}

	api := operations.NewConsoleAPI(swaggerSpec)

	if !serverDebugLog {
		// Disable console logging if server debug log is not enabled
		noLog := func(string, ...interface{}) {}

		consoleapi.LogInfo = noLog
		consoleapi.LogError = noLog
		api.Logger = noLog
	}

	// Pass in console application config. This needs to happen before the
	// ConfigureAPI() call.
	consoleapi.GlobalMinIOConfig = consoleapi.MinIOConfig{
		OpenIDProviders: buildOpenIDConsoleConfig(),
	}

	server := consoleapi.NewServer(api)
	// register all APIs
	server.ConfigureAPI()

	consolePort, _ := strconv.Atoi(globalMinioConsolePort)

	server.Host = globalMinioConsoleHost
	server.Port = consolePort
	consoleapi.Port = globalMinioConsolePort
	consoleapi.Hostname = globalMinioConsoleHost

	if globalIsTLS {
		// If TLS certificates are provided enforce the HTTPS.
		server.EnabledListeners = []string{"https"}
		server.TLSPort = consolePort
		// Need to store tls-port, tls-host un config variables so secure.middleware can read from there
		consoleapi.TLSPort = globalMinioConsolePort
		consoleapi.Hostname = globalMinioConsoleHost
	}

	return server, nil
}

// Check for updates and print a notification message
func checkUpdate(mode string) {
	updateURL := minioReleaseInfoURL
	if runtime.GOOS == globalWindowsOSName {
		updateURL = minioReleaseWindowsInfoURL
	}

	u, err := url.Parse(updateURL)
	if err != nil {
		return
	}

	if currentReleaseTime.IsZero() {
		return
	}

	_, lrTime, err := getLatestReleaseTime(u, 2*time.Second, mode)
	if err != nil {
		return
	}

	var older time.Duration
	var downloadURL string
	if lrTime.After(currentReleaseTime) {
		older = lrTime.Sub(currentReleaseTime)
		downloadURL = getDownloadURL(releaseTimeToReleaseTag(lrTime))
	}

	updateMsg := prepareUpdateMessage(downloadURL, older)
	if updateMsg == "" {
		return
	}

	logger.Info(prepareUpdateMessage("Run `mc admin update ALIAS`", lrTime.Sub(currentReleaseTime)))
}

func newConfigDir(dir string, dirSet bool, getDefaultDir func() string) (*ConfigDir, error) {
	if dir == "" {
		dir = getDefaultDir()
	}

	if dir == "" {
		if !dirSet {
			return nil, fmt.Errorf("missing option must be provided")
		}
		return nil, fmt.Errorf("provided option cannot be empty")
	}

	// Disallow relative paths, figure out absolute paths.
	dirAbs, err := filepath.Abs(dir)
	if err != nil {
		return nil, err
	}
	err = mkdirAllIgnorePerm(dirAbs)
	if err != nil {
		return nil, fmt.Errorf("unable to create the directory `%s`: %w", dirAbs, err)
	}

	return &ConfigDir{path: dirAbs}, nil
}

func buildServerCtxt(ctx *cli.Context, ctxt *serverCtxt) (err error) {
	// Get "json" flag from command line argument and
	ctxt.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json")
	// Get quiet flag from command line argument.
	ctxt.Quiet = ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
	// Get anonymous flag from command line argument.
	ctxt.Anonymous = ctx.IsSet("anonymous") || ctx.GlobalIsSet("anonymous")
	// Fetch address option
	ctxt.Addr = ctx.GlobalString("address")
	if ctxt.Addr == "" || ctxt.Addr == ":"+GlobalMinioDefaultPort {
		ctxt.Addr = ctx.String("address")
	}

	// Fetch console address option
	ctxt.ConsoleAddr = ctx.GlobalString("console-address")
	if ctxt.ConsoleAddr == "" {
		ctxt.ConsoleAddr = ctx.String("console-address")
	}

	// Check "no-compat" flag from command line argument.
	ctxt.StrictS3Compat = !(ctx.IsSet("no-compat") || ctx.GlobalIsSet("no-compat"))

	switch {
	case ctx.IsSet("config-dir"):
		ctxt.ConfigDir = ctx.String("config-dir")
		ctxt.configDirSet = true
	case ctx.GlobalIsSet("config-dir"):
		ctxt.ConfigDir = ctx.GlobalString("config-dir")
		ctxt.configDirSet = true
	}

	switch {
	case ctx.IsSet("certs-dir"):
		ctxt.CertsDir = ctx.String("certs-dir")
		ctxt.certsDirSet = true
	case ctx.GlobalIsSet("certs-dir"):
		ctxt.CertsDir = ctx.GlobalString("certs-dir")
		ctxt.certsDirSet = true
	}

	ctxt.FTP = ctx.StringSlice("ftp")
	ctxt.SFTP = ctx.StringSlice("sftp")

	ctxt.Interface = ctx.String("interface")
	ctxt.UserTimeout = ctx.Duration("conn-user-timeout")
	ctxt.ConnReadDeadline = ctx.Duration("conn-read-deadline")
	ctxt.ConnWriteDeadline = ctx.Duration("conn-write-deadline")
	ctxt.ConnClientReadDeadline = ctx.Duration("conn-client-read-deadline")

	ctxt.ShutdownTimeout = ctx.Duration("shutdown-timeout")
	ctxt.IdleTimeout = ctx.Duration("idle-timeout")
	ctxt.ReadHeaderTimeout = ctx.Duration("read-header-timeout")
	ctxt.MaxIdleConnsPerHost = ctx.Int("max-idle-conns-per-host")

	if conf := ctx.String("config"); len(conf) > 0 {
		err = mergeServerCtxtFromConfigFile(conf, ctxt)
	} else {
		err = mergeDisksLayoutFromArgs(serverCmdArgs(ctx), ctxt)
	}

	return err
}

func handleCommonArgs(ctxt serverCtxt) {
	if ctxt.JSON {
		logger.EnableJSON()
	}
	if ctxt.Quiet {
		logger.EnableQuiet()
	}
	if ctxt.Anonymous {
		logger.EnableAnonymous()
	}

	consoleAddr := ctxt.ConsoleAddr
	addr := ctxt.Addr
	configDir := ctxt.ConfigDir
	configSet := ctxt.configDirSet
	certsDir := ctxt.CertsDir
	certsSet := ctxt.certsDirSet

	if consoleAddr == "" {
		p, err := xnet.GetFreePort()
		if err != nil {
			logger.FatalIf(err, "Unable to get free port for Console UI on the host")
		}
		// hold the port
		l, err := net.Listen("TCP", fmt.Sprintf(":%s", p.String()))
		if err == nil {
			defer l.Close()
		}
		consoleAddr = net.JoinHostPort("", p.String())
	}

	if _, _, err := net.SplitHostPort(consoleAddr); err != nil {
		logger.FatalIf(err, "Unable to start listening on console port")
	}

	if consoleAddr == addr {
		logger.FatalIf(errors.New("--console-address cannot be same as --address"), "Unable to start the server")
	}

	globalMinioHost, globalMinioPort = mustSplitHostPort(addr)
	if globalMinioPort == "0" {
		p, err := xnet.GetFreePort()
		if err != nil {
			logger.FatalIf(err, "Unable to get free port for S3 API on the host")
		}
		globalMinioPort = p.String()
		globalDynamicAPIPort = true
	}

	globalMinioConsoleHost, globalMinioConsolePort = mustSplitHostPort(consoleAddr)

	if globalMinioPort == globalMinioConsolePort {
		logger.FatalIf(errors.New("--console-address port cannot be same as --address port"), "Unable to start the server")
	}

	globalMinioAddr = addr

	// Set all config, certs and CAs directories.
	var err error
	globalConfigDir, err = newConfigDir(configDir, configSet, defaultConfigDir.Get)
	logger.FatalIf(err, "Unable to initialize the (deprecated) config directory")
	globalCertsDir, err = newConfigDir(certsDir, certsSet, defaultCertsDir.Get)
	logger.FatalIf(err, "Unable to initialize the certs directory")

	// Remove this code when we deprecate and remove config-dir.
	// This code is to make sure we inherit from the config-dir
	// option if certs-dir is not provided.
	if !certsSet && configSet {
		globalCertsDir = &ConfigDir{path: filepath.Join(globalConfigDir.Get(), certsDir)}
	}

	globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}

	logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
}

func runDNSCache(ctx *cli.Context) {
	dnsTTL := ctx.Duration("dns-cache-ttl")
	// Check if we have configured a custom DNS cache TTL.
	if dnsTTL <= 0 {
		dnsTTL = 10 * time.Minute
	}

	// Call to refresh will refresh names in cache.
	go func() {
		// Baremetal setups set DNS refresh window up to dnsTTL duration.
		t := time.NewTicker(dnsTTL)
		defer t.Stop()
		for {
			select {
			case <-t.C:
				globalDNSCache.Refresh()

			case <-GlobalContext.Done():
				return
			}
		}
	}()
}

type envKV struct {
	Key   string
	Value string
	Skip  bool
}

func (e envKV) String() string {
	if e.Skip {
		return ""
	}
	return fmt.Sprintf("%s=%s", e.Key, e.Value)
}

func parsEnvEntry(envEntry string) (envKV, error) {
	envEntry = strings.TrimSpace(envEntry)
	if envEntry == "" {
		// Skip all empty lines
		return envKV{
			Skip: true,
		}, nil
	}
	if strings.HasPrefix(envEntry, "#") {
		// Skip commented lines
		return envKV{
			Skip: true,
		}, nil
	}
	envTokens := strings.SplitN(strings.TrimSpace(strings.TrimPrefix(envEntry, "export")), config.EnvSeparator, 2)
	if len(envTokens) != 2 {
		return envKV{}, fmt.Errorf("envEntry malformed; %s, expected to be of form 'KEY=value'", envEntry)
	}

	key := envTokens[0]
	val := envTokens[1]

	// Remove quotes from the value if found
	if len(val) >= 2 {
		quote := val[0]
		if (quote == '"' || quote == '\'') && val[len(val)-1] == quote {
			val = val[1 : len(val)-1]
		}
	}

	return envKV{
		Key:   key,
		Value: val,
	}, nil
}

// Similar to os.Environ returns a copy of strings representing
// the environment values from a file, in the form "key, value".
// in a structured form.
func minioEnvironFromFile(envConfigFile string) ([]envKV, error) {
	f, err := Open(envConfigFile)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	var ekvs []envKV
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		ekv, err := parsEnvEntry(scanner.Text())
		if err != nil {
			return nil, err
		}
		if ekv.Skip {
			// Skips empty lines
			continue
		}
		ekvs = append(ekvs, ekv)
	}
	if err = scanner.Err(); err != nil {
		return nil, err
	}
	return ekvs, nil
}

func readFromSecret(sp string) (string, error) {
	// Supports reading path from docker secrets, filename is
	// relative to /run/secrets/ position.
	if isFile(pathJoin("/run/secrets/", sp)) {
		sp = pathJoin("/run/secrets/", sp)
	}
	credBuf, err := os.ReadFile(sp)
	if err != nil {
		if os.IsNotExist(err) { // ignore if file doesn't exist.
			return "", nil
		}
		return "", err
	}
	return string(bytes.TrimSpace(credBuf)), nil
}

func loadEnvVarsFromFiles() {
	if env.IsSet(config.EnvAccessKeyFile) {
		accessKey, err := readFromSecret(env.Get(config.EnvAccessKeyFile, ""))
		if err != nil {
			logger.Fatal(config.ErrInvalidCredentials(err),
				"Unable to validate credentials inherited from the secret file(s)")
		}
		if accessKey != "" {
			os.Setenv(config.EnvRootUser, accessKey)
		}
	}

	if env.IsSet(config.EnvSecretKeyFile) {
		secretKey, err := readFromSecret(env.Get(config.EnvSecretKeyFile, ""))
		if err != nil {
			logger.Fatal(config.ErrInvalidCredentials(err),
				"Unable to validate credentials inherited from the secret file(s)")
		}
		if secretKey != "" {
			os.Setenv(config.EnvRootPassword, secretKey)
		}
	}

	if env.IsSet(config.EnvRootUserFile) {
		rootUser, err := readFromSecret(env.Get(config.EnvRootUserFile, ""))
		if err != nil {
			logger.Fatal(config.ErrInvalidCredentials(err),
				"Unable to validate credentials inherited from the secret file(s)")
		}
		if rootUser != "" {
			os.Setenv(config.EnvRootUser, rootUser)
		}
	}

	if env.IsSet(config.EnvRootPasswordFile) {
		rootPassword, err := readFromSecret(env.Get(config.EnvRootPasswordFile, ""))
		if err != nil {
			logger.Fatal(config.ErrInvalidCredentials(err),
				"Unable to validate credentials inherited from the secret file(s)")
		}
		if rootPassword != "" {
			os.Setenv(config.EnvRootPassword, rootPassword)
		}
	}

	if env.IsSet(kms.EnvKMSSecretKeyFile) {
		kmsSecret, err := readFromSecret(env.Get(kms.EnvKMSSecretKeyFile, ""))
		if err != nil {
			logger.Fatal(err, "Unable to read the KMS secret key inherited from secret file")
		}
		if kmsSecret != "" {
			os.Setenv(kms.EnvKMSSecretKey, kmsSecret)
		}
	}

	if env.IsSet(config.EnvConfigEnvFile) {
		ekvs, err := minioEnvironFromFile(env.Get(config.EnvConfigEnvFile, ""))
		if err != nil && !os.IsNotExist(err) {
			logger.Fatal(err, "Unable to read the config environment file")
		}
		for _, ekv := range ekvs {
			os.Setenv(ekv.Key, ekv.Value)
		}
	}
}

func serverHandleEnvVars() {
	var err error
	globalBrowserEnabled, err = config.ParseBool(env.Get(config.EnvBrowser, config.EnableOn))
	if err != nil {
		logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
	}
	if globalBrowserEnabled {
		if redirectURL := env.Get(config.EnvBrowserRedirectURL, ""); redirectURL != "" {
			u, err := xnet.ParseHTTPURL(redirectURL)
			if err != nil {
				logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable")
			}
			// Look for if URL has invalid values and return error.
			if !((u.Scheme == "http" || u.Scheme == "https") &&
				u.Opaque == "" &&
				!u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
				err := fmt.Errorf("URL contains unexpected resources, expected URL to be one of http(s)://console.example.com or as a subpath via API endpoint http(s)://minio.example.com/minio format: %v", u)
				logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value is environment variable")
			}
			globalBrowserRedirectURL = u
		}
		globalBrowserRedirect = env.Get(config.EnvBrowserRedirect, config.EnableOn) == config.EnableOn
	}

	if serverURL := env.Get(config.EnvMinIOServerURL, ""); serverURL != "" {
		u, err := xnet.ParseHTTPURL(serverURL)
		if err != nil {
			logger.Fatal(err, "Invalid MINIO_SERVER_URL value in environment variable")
		}
		// Look for if URL has invalid values and return error.
		if !((u.Scheme == "http" || u.Scheme == "https") &&
			(u.Path == "/" || u.Path == "") && u.Opaque == "" &&
			!u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
			err := fmt.Errorf("URL contains unexpected resources, expected URL to be of http(s)://minio.example.com format: %v", u)
			logger.Fatal(err, "Invalid MINIO_SERVER_URL value is environment variable")
		}
		u.Path = "" // remove any path component such as `/`
		globalMinioEndpoint = u.String()
		globalMinioEndpointURL = u
	}

	globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
	if err != nil {
		logger.Fatal(config.ErrInvalidFSOSyncValue(err), "Invalid MINIO_FS_OSYNC value in environment variable")
	}

	rootDiskSize := env.Get(config.EnvRootDriveThresholdSize, "")
	if rootDiskSize == "" {
		rootDiskSize = env.Get(config.EnvRootDiskThresholdSize, "")
	}
	if rootDiskSize != "" {
		size, err := humanize.ParseBytes(rootDiskSize)
		if err != nil {
			logger.Fatal(err, fmt.Sprintf("Invalid %s value in root drive threshold environment variable", rootDiskSize))
		}
		globalRootDiskThreshold = size
	}

	domains := env.Get(config.EnvDomain, "")
	if len(domains) != 0 {
		for _, domainName := range strings.Split(domains, config.ValueSeparator) {
			if _, ok := dns2.IsDomainName(domainName); !ok {
				logger.Fatal(config.ErrInvalidDomainValue(nil).Msg("Unknown value `%s`", domainName),
					"Invalid MINIO_DOMAIN value in environment variable")
			}
			globalDomainNames = append(globalDomainNames, domainName)
		}
		sort.Strings(globalDomainNames)
		lcpSuf := lcpSuffix(globalDomainNames)
		for _, domainName := range globalDomainNames {
			if domainName == lcpSuf && len(globalDomainNames) > 1 {
				logger.Fatal(config.ErrOverlappingDomainValue(nil).Msg("Overlapping domains `%s` not allowed", globalDomainNames),
					"Invalid MINIO_DOMAIN value in environment variable")
			}
		}
	}

	publicIPs := env.Get(config.EnvPublicIPs, "")
	if len(publicIPs) != 0 {
		minioEndpoints := strings.Split(publicIPs, config.ValueSeparator)
		domainIPs := set.NewStringSet()
		for _, endpoint := range minioEndpoints {
			if net.ParseIP(endpoint) == nil {
				// Checking if the IP is a DNS entry.
				lookupHost := globalDNSCache.LookupHost
				if IsKubernetes() || IsDocker() {
					lookupHost = net.DefaultResolver.LookupHost
				}

				addrs, err := lookupHost(GlobalContext, endpoint)
				if err != nil {
					logger.FatalIf(err, "Unable to initialize MinIO server with [%s] invalid entry found in MINIO_PUBLIC_IPS", endpoint)
				}
				for _, addr := range addrs {
					domainIPs.Add(addr)
				}
			}
			domainIPs.Add(endpoint)
		}
		updateDomainIPs(domainIPs)
	} else {
		// Add found interfaces IP address to global domain IPS,
		// loopback addresses will be naturally dropped.
		domainIPs := mustGetLocalIP4()
		for _, host := range globalEndpoints.Hostnames() {
			domainIPs.Add(host)
		}
		updateDomainIPs(domainIPs)
	}

	// In place update is true by default if the MINIO_UPDATE is not set
	// or is not set to 'off', if MINIO_UPDATE is set to 'off' then
	// in-place update is off.
	globalInplaceUpdateDisabled = strings.EqualFold(env.Get(config.EnvUpdate, config.EnableOn), config.EnableOff)

	// Check if the supported credential env vars,
	// "MINIO_ROOT_USER" and "MINIO_ROOT_PASSWORD" are provided
	// Warn user if deprecated environment variables,
	// "MINIO_ACCESS_KEY" and "MINIO_SECRET_KEY", are defined
	// Check all error conditions first
	//nolint:gocritic
	if !env.IsSet(config.EnvRootUser) && env.IsSet(config.EnvRootPassword) {
		logger.Fatal(config.ErrMissingEnvCredentialRootUser(nil), "Unable to start MinIO")
	} else if env.IsSet(config.EnvRootUser) && !env.IsSet(config.EnvRootPassword) {
		logger.Fatal(config.ErrMissingEnvCredentialRootPassword(nil), "Unable to start MinIO")
	} else if !env.IsSet(config.EnvRootUser) && !env.IsSet(config.EnvRootPassword) {
		if !env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
			logger.Fatal(config.ErrMissingEnvCredentialAccessKey(nil), "Unable to start MinIO")
		} else if env.IsSet(config.EnvAccessKey) && !env.IsSet(config.EnvSecretKey) {
			logger.Fatal(config.ErrMissingEnvCredentialSecretKey(nil), "Unable to start MinIO")
		}
	}

	globalDisableFreezeOnBoot = env.Get("_MINIO_DISABLE_API_FREEZE_ON_BOOT", "") == "true" || serverDebugLog
}

func loadRootCredentials() {
	// At this point, either both environment variables
	// are defined or both are not defined.
	// Check both cases and authenticate them if correctly defined
	var user, password string
	var hasCredentials bool
	//nolint:gocritic
	if env.IsSet(config.EnvRootUser) && env.IsSet(config.EnvRootPassword) {
		user = env.Get(config.EnvRootUser, "")
		password = env.Get(config.EnvRootPassword, "")
		hasCredentials = true
	} else if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
		user = env.Get(config.EnvAccessKey, "")
		password = env.Get(config.EnvSecretKey, "")
		hasCredentials = true
	} else if globalServerCtxt.RootUser != "" && globalServerCtxt.RootPwd != "" {
		user, password = globalServerCtxt.RootUser, globalServerCtxt.RootPwd
		hasCredentials = true
	}
	if hasCredentials {
		cred, err := auth.CreateCredentials(user, password)
		if err != nil {
			logger.Fatal(config.ErrInvalidCredentials(err),
				"Unable to validate credentials inherited from the shell environment")
		}
		if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
			msg := fmt.Sprintf("WARNING: %s and %s are deprecated.\n"+
				"         Please use %s and %s",
				config.EnvAccessKey, config.EnvSecretKey,
				config.EnvRootUser, config.EnvRootPassword)
			logger.Info(color.RedBold(msg))
		}
		globalActiveCred = cred
		globalCredViaEnv = true
	} else {
		globalActiveCred = auth.DefaultCredentials
	}
}

// Initialize KMS global variable after valiadating and loading the configuration.
// It depends on KMS env variables and global cli flags.
func handleKMSConfig() {
	if env.IsSet(kms.EnvKMSSecretKey) && env.IsSet(kms.EnvKESEndpoint) {
		logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKMSSecretKey, kms.EnvKESEndpoint))
	}

	if env.IsSet(kms.EnvKMSSecretKey) {
		KMS, err := kms.Parse(env.Get(kms.EnvKMSSecretKey, ""))
		if err != nil {
			logger.Fatal(err, "Unable to parse the KMS secret key inherited from the shell environment")
		}
		GlobalKMS = KMS
	}
	if env.IsSet(kms.EnvKESEndpoint) {
		if env.IsSet(kms.EnvKESAPIKey) {
			if env.IsSet(kms.EnvKESClientKey) {
				logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientKey))
			}
			if env.IsSet(kms.EnvKESClientCert) {
				logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientCert))
			}
		}
		if !env.IsSet(kms.EnvKESKeyName) {
			logger.Fatal(errors.New("Invalid KES configuration"), fmt.Sprintf("The mandatory environment variable %q not set", kms.EnvKESKeyName))
		}

		var endpoints []string
		for _, endpoint := range strings.Split(env.Get(kms.EnvKESEndpoint, ""), ",") {
			if strings.TrimSpace(endpoint) == "" {
				continue
			}
			if !ellipses.HasEllipses(endpoint) {
				endpoints = append(endpoints, endpoint)
				continue
			}
			patterns, err := ellipses.FindEllipsesPatterns(endpoint)
			if err != nil {
				logger.Fatal(err, fmt.Sprintf("Invalid KES endpoint %q", endpoint))
			}
			for _, lbls := range patterns.Expand() {
				endpoints = append(endpoints, strings.Join(lbls, ""))
			}
		}
		rootCAs, err := certs.GetRootCAs(env.Get(kms.EnvKESServerCA, globalCertsCADir.Get()))
		if err != nil {
			logger.Fatal(err, fmt.Sprintf("Unable to load X.509 root CAs for KES from %q", env.Get(kms.EnvKESServerCA, globalCertsCADir.Get())))
		}

		var kmsConf kms.Config
		if env.IsSet(kms.EnvKESAPIKey) {
			key, err := kes.ParseAPIKey(env.Get(kms.EnvKESAPIKey, ""))
			if err != nil {
				logger.Fatal(err, fmt.Sprintf("Failed to parse KES API key from %q", env.Get(kms.EnvKESAPIKey, "")))
			}
			kmsConf = kms.Config{
				Endpoints:    endpoints,
				Enclave:      env.Get(kms.EnvKESEnclave, ""),
				DefaultKeyID: env.Get(kms.EnvKESKeyName, ""),
				APIKey:       key,
				RootCAs:      rootCAs,
			}
		} else {
			loadX509KeyPair := func(certFile, keyFile string) (tls.Certificate, error) {
				// Manually load the certificate and private key into memory.
				// We need to check whether the private key is encrypted, and
				// if so, decrypt it using the user-provided password.
				certBytes, err := os.ReadFile(certFile)
				if err != nil {
					return tls.Certificate{}, fmt.Errorf("Unable to load KES client certificate as specified by the shell environment: %v", err)
				}
				keyBytes, err := os.ReadFile(keyFile)
				if err != nil {
					return tls.Certificate{}, fmt.Errorf("Unable to load KES client private key as specified by the shell environment: %v", err)
				}
				privateKeyPEM, rest := pem.Decode(bytes.TrimSpace(keyBytes))
				if len(rest) != 0 {
					return tls.Certificate{}, errors.New("Unable to load KES client private key as specified by the shell environment: private key contains additional data")
				}
				if x509.IsEncryptedPEMBlock(privateKeyPEM) {
					keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(kms.EnvKESClientPassword, "")))
					if err != nil {
						return tls.Certificate{}, fmt.Errorf("Unable to decrypt KES client private key as specified by the shell environment: %v", err)
					}
					keyBytes = pem.EncodeToMemory(&pem.Block{Type: privateKeyPEM.Type, Bytes: keyBytes})
				}
				certificate, err := tls.X509KeyPair(certBytes, keyBytes)
				if err != nil {
					return tls.Certificate{}, fmt.Errorf("Unable to load KES client certificate as specified by the shell environment: %v", err)
				}
				return certificate, nil
			}

			reloadCertEvents := make(chan tls.Certificate, 1)
			certificate, err := certs.NewCertificate(env.Get(kms.EnvKESClientCert, ""), env.Get(kms.EnvKESClientKey, ""), loadX509KeyPair)
			if err != nil {
				logger.Fatal(err, "Failed to load KES client certificate")
			}
			certificate.Watch(context.Background(), 15*time.Minute, syscall.SIGHUP)
			certificate.Notify(reloadCertEvents)

			kmsConf = kms.Config{
				Endpoints:        endpoints,
				Enclave:          env.Get(kms.EnvKESEnclave, ""),
				DefaultKeyID:     env.Get(kms.EnvKESKeyName, ""),
				Certificate:      certificate,
				ReloadCertEvents: reloadCertEvents,
				RootCAs:          rootCAs,
			}
		}

		KMS, err := kms.NewWithConfig(kmsConf)
		if err != nil {
			logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
		}
		// We check that the default key ID exists or try to create it otherwise.
		// This implicitly checks that we can communicate to KES. We don't treat
		// a policy error as failure condition since MinIO may not have the permission
		// to create keys - just to generate/decrypt data encryption keys.
		if err = KMS.CreateKey(context.Background(), env.Get(kms.EnvKESKeyName, "")); err != nil && !errors.Is(err, kes.ErrKeyExists) && !errors.Is(err, kes.ErrNotAllowed) {
			logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
		}
		GlobalKMS = KMS
	}
}

func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
	if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
		return nil, nil, false, nil
	}

	if x509Certs, err = config.ParsePublicCertFile(getPublicCertFile()); err != nil {
		return nil, nil, false, err
	}

	manager, err = certs.NewManager(GlobalContext, getPublicCertFile(), getPrivateKeyFile(), config.LoadX509KeyPair)
	if err != nil {
		return nil, nil, false, err
	}

	// MinIO has support for multiple certificates. It expects the following structure:
	//  certs/
	//   │
	//   ├─ public.crt
	//   ├─ private.key
	//   │
	//   ├─ example.com/
	//   │   │
	//   │   ├─ public.crt
	//   │   └─ private.key
	//   └─ foobar.org/
	//      │
	//      ├─ public.crt
	//      └─ private.key
	//   ...
	//
	// Therefore, we read all filenames in the cert directory and check
	// for each directory whether it contains a public.crt and private.key.
	// If so, we try to add it to certificate manager.
	root, err := Open(globalCertsDir.Get())
	if err != nil {
		return nil, nil, false, err
	}
	defer root.Close()

	files, err := root.Readdir(-1)
	if err != nil {
		return nil, nil, false, err
	}
	for _, file := range files {
		// Ignore all
		// - regular files
		// - "CAs" directory
		// - any directory which starts with ".."
		if file.Mode().IsRegular() || file.Name() == "CAs" || strings.HasPrefix(file.Name(), "..") {
			continue
		}
		if file.Mode()&os.ModeSymlink == os.ModeSymlink {
			file, err = Stat(filepath.Join(root.Name(), file.Name()))
			if err != nil {
				// not accessible ignore
				continue
			}
			if !file.IsDir() {
				continue
			}
		}

		var (
			certFile = filepath.Join(root.Name(), file.Name(), publicCertFile)
			keyFile  = filepath.Join(root.Name(), file.Name(), privateKeyFile)
		)
		if !isFile(certFile) || !isFile(keyFile) {
			continue
		}
		if err = manager.AddCertificate(certFile, keyFile); err != nil {
			err = fmt.Errorf("Unable to load TLS certificate '%s,%s': %w", certFile, keyFile, err)
			logger.LogIf(GlobalContext, err, logger.ErrorKind)
		}
	}
	secureConn = true

	// Certs might be symlinks, reload them every 10 seconds.
	manager.UpdateReloadDuration(10 * time.Second)

	// syscall.SIGHUP to reload the certs.
	manager.ReloadOnSignal(syscall.SIGHUP)

	return x509Certs, manager, secureConn, nil
}

// contextCanceled returns whether a context is canceled.
func contextCanceled(ctx context.Context) bool {
	select {
	case <-ctx.Done():
		return true
	default:
		return false
	}
}

// bgContext returns a context that can be used for async operations.
// Cancellation/timeouts are removed, so parent cancellations/timeout will
// not propagate from parent.
// Context values are preserved.
// This can be used for goroutines that live beyond the parent context.
func bgContext(parent context.Context) context.Context {
	return bgCtx{parent: parent}
}

type bgCtx struct {
	parent context.Context
}

func (a bgCtx) Done() <-chan struct{} {
	return nil
}

func (a bgCtx) Err() error {
	return nil
}

func (a bgCtx) Deadline() (deadline time.Time, ok bool) {
	return time.Time{}, false
}

func (a bgCtx) Value(key interface{}) interface{} {
	return a.parent.Value(key)
}