github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/utils.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  	"bytes"
    22  	"crypto/tls"
    23  	"errors"
    24  	"fmt"
    25  	"math"
    26  	"math/rand"
    27  	"net"
    28  	"net/http"
    29  	"os"
    30  	"path/filepath"
    31  	"regexp"
    32  	"strconv"
    33  	"strings"
    34  	"time"
    35  
    36  	"github.com/mattn/go-ieproxy"
    37  	"github.com/minio/madmin-go/v3"
    38  	"github.com/minio/minio-go/v7"
    39  
    40  	jwtgo "github.com/golang-jwt/jwt/v4"
    41  	"github.com/minio/mc/pkg/probe"
    42  	"github.com/minio/pkg/v2/console"
    43  )
    44  
    45  func isErrIgnored(err *probe.Error) (ignored bool) {
    46  	// For all non critical errors we can continue for the remaining files.
    47  	switch e := err.ToGoError().(type) {
    48  	// Handle these specifically for filesystem related errors.
    49  	case BrokenSymlink, TooManyLevelsSymlink, PathNotFound:
    50  		ignored = true
    51  	// Handle these specifically for object storage related errors.
    52  	case BucketNameEmpty, ObjectMissing, ObjectAlreadyExists:
    53  		ignored = true
    54  	case ObjectAlreadyExistsAsDirectory, BucketDoesNotExist, BucketInvalid:
    55  		ignored = true
    56  	case minio.ErrorResponse:
    57  		ignored = strings.Contains(e.Error(), "The specified key does not exist")
    58  	default:
    59  		ignored = false
    60  	}
    61  	return ignored
    62  }
    63  
    64  const (
    65  	letterBytes   = "abcdefghijklmnopqrstuvwxyz01234569"
    66  	letterIdxBits = 6                    // 6 bits to represent a letter index
    67  	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
    68  	letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
    69  )
    70  
    71  // UTCNow - returns current UTC time.
    72  func UTCNow() time.Time {
    73  	return time.Now().UTC()
    74  }
    75  
    76  func max(a, b int) int {
    77  	if a > b {
    78  		return a
    79  	}
    80  	return b
    81  }
    82  
    83  // randString generates random names and prepends them with a known prefix.
    84  func randString(n int, src rand.Source, prefix string) string {
    85  	if n == 0 {
    86  		return prefix
    87  	}
    88  	b := make([]byte, n)
    89  	// A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
    90  	for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
    91  		if remain == 0 {
    92  			cache, remain = src.Int63(), letterIdxMax
    93  		}
    94  		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
    95  			b[i] = letterBytes[idx]
    96  			i--
    97  		}
    98  		cache >>= letterIdxBits
    99  		remain--
   100  	}
   101  	x := n / 2
   102  	if x == 0 {
   103  		x = 1
   104  	}
   105  	return prefix + string(b[0:x])
   106  }
   107  
   108  // printTLSCertInfo prints some fields of the certificates received from the server.
   109  // Fields will be inspected by the user, so they must be conscise and useful
   110  func printTLSCertInfo(t *tls.ConnectionState) {
   111  	if globalDebug {
   112  		for _, cert := range t.PeerCertificates {
   113  			console.Debugln("TLS Certificate found: ")
   114  			if len(cert.Issuer.Country) > 0 {
   115  				console.Debugln(" >> Country: " + cert.Issuer.Country[0])
   116  			}
   117  			if len(cert.Issuer.Organization) > 0 {
   118  				console.Debugln(" >> Organization: " + cert.Issuer.Organization[0])
   119  			}
   120  			console.Debugln(" >> Expires: " + cert.NotAfter.String())
   121  		}
   122  	}
   123  }
   124  
   125  // splitStr splits a string into n parts, empty strings are added
   126  // if we are not able to reach n elements
   127  func splitStr(path, sep string, n int) []string {
   128  	splits := strings.SplitN(path, sep, n)
   129  	// Add empty strings if we found elements less than nr
   130  	for i := n - len(splits); i > 0; i-- {
   131  		splits = append(splits, "")
   132  	}
   133  	return splits
   134  }
   135  
   136  // NewS3Config simply creates a new Config struct using the passed
   137  // parameters.
   138  func NewS3Config(alias, urlStr string, aliasCfg *aliasConfigV10) *Config {
   139  	// We have a valid alias and hostConfig. We populate the
   140  	// credentials from the match found in the config file.
   141  	s3Config := new(Config)
   142  
   143  	s3Config.AppName = filepath.Base(os.Args[0])
   144  	s3Config.AppVersion = ReleaseTag
   145  	s3Config.Debug = globalDebug
   146  	s3Config.Insecure = globalInsecure
   147  	s3Config.ConnReadDeadline = globalConnReadDeadline
   148  	s3Config.ConnWriteDeadline = globalConnWriteDeadline
   149  	s3Config.UploadLimit = int64(globalLimitUpload)
   150  	s3Config.DownloadLimit = int64(globalLimitDownload)
   151  
   152  	s3Config.HostURL = urlStr
   153  	s3Config.Alias = alias
   154  	if aliasCfg != nil {
   155  		s3Config.AccessKey = aliasCfg.AccessKey
   156  		s3Config.SecretKey = aliasCfg.SecretKey
   157  		s3Config.SessionToken = aliasCfg.SessionToken
   158  		s3Config.Signature = aliasCfg.API
   159  		s3Config.Lookup = getLookupType(aliasCfg.Path)
   160  	}
   161  	return s3Config
   162  }
   163  
   164  // lineTrunc - truncates a string to the given maximum length by
   165  // adding ellipsis in the middle
   166  func lineTrunc(content string, maxLen int) string {
   167  	runes := []rune(content)
   168  	rlen := len(runes)
   169  	if rlen <= maxLen {
   170  		return content
   171  	}
   172  	halfLen := maxLen / 2
   173  	fstPart := string(runes[0:halfLen])
   174  	sndPart := string(runes[rlen-halfLen:])
   175  	return fstPart + "…" + sndPart
   176  }
   177  
   178  // isOlder returns true if the passed object is older than olderRef
   179  func isOlder(ti time.Time, olderRef string) bool {
   180  	if olderRef == "" {
   181  		return false
   182  	}
   183  	objectAge := time.Since(ti)
   184  	olderThan, e := ParseDuration(olderRef)
   185  	fatalIf(probe.NewError(e), "Unable to parse olderThan=`"+olderRef+"`.")
   186  	return objectAge < time.Duration(olderThan)
   187  }
   188  
   189  // isNewer returns true if the passed object is newer than newerRef
   190  func isNewer(ti time.Time, newerRef string) bool {
   191  	if newerRef == "" {
   192  		return false
   193  	}
   194  
   195  	objectAge := time.Since(ti)
   196  	newerThan, e := ParseDuration(newerRef)
   197  	fatalIf(probe.NewError(e), "Unable to parse newerThan=`"+newerRef+"`.")
   198  	return objectAge >= time.Duration(newerThan)
   199  }
   200  
   201  // getLookupType returns the minio.BucketLookupType for lookup
   202  // option entered on the command line
   203  func getLookupType(l string) minio.BucketLookupType {
   204  	l = strings.ToLower(l)
   205  	switch l {
   206  	case "off":
   207  		return minio.BucketLookupDNS
   208  	case "on":
   209  		return minio.BucketLookupPath
   210  	}
   211  	return minio.BucketLookupAuto
   212  }
   213  
   214  // Return true if target url is a part of a source url such as:
   215  // alias/bucket/ and alias/bucket/dir/, however
   216  func isURLContains(srcURL, tgtURL, sep string) bool {
   217  	// Add a separator to source url if not found
   218  	if !strings.HasSuffix(srcURL, sep) {
   219  		srcURL += sep
   220  	}
   221  	if !strings.HasSuffix(tgtURL, sep) {
   222  		tgtURL += sep
   223  	}
   224  	// Check if we are going to copy a directory into itself
   225  	if strings.HasPrefix(tgtURL, srcURL) {
   226  		return true
   227  	}
   228  	return false
   229  }
   230  
   231  // ErrInvalidFileSystemAttribute reflects invalid fily system attribute
   232  var ErrInvalidFileSystemAttribute = errors.New("Error in parsing file system attribute")
   233  
   234  func parseAtimeMtime(attr map[string]string) (atime, mtime time.Time, err *probe.Error) {
   235  	if val, ok := attr["atime"]; ok {
   236  		vals := strings.SplitN(val, "#", 2)
   237  		atim, e := strconv.ParseInt(vals[0], 10, 64)
   238  		if e != nil {
   239  			return atime, mtime, probe.NewError(e)
   240  		}
   241  		var atimnsec int64
   242  		if len(vals) == 2 {
   243  			atimnsec, e = strconv.ParseInt(vals[1], 10, 64)
   244  			if e != nil {
   245  				return atime, mtime, probe.NewError(e)
   246  			}
   247  		}
   248  		atime = time.Unix(atim, atimnsec)
   249  	}
   250  
   251  	if val, ok := attr["mtime"]; ok {
   252  		vals := strings.SplitN(val, "#", 2)
   253  		mtim, e := strconv.ParseInt(vals[0], 10, 64)
   254  		if e != nil {
   255  			return atime, mtime, probe.NewError(e)
   256  		}
   257  		var mtimnsec int64
   258  		if len(vals) == 2 {
   259  			mtimnsec, e = strconv.ParseInt(vals[1], 10, 64)
   260  			if e != nil {
   261  				return atime, mtime, probe.NewError(e)
   262  			}
   263  		}
   264  		mtime = time.Unix(mtim, mtimnsec)
   265  	}
   266  	return atime, mtime, nil
   267  }
   268  
   269  // Returns a map by parsing the value of X-Amz-Meta-Mc-Attrs/X-Amz-Meta-s3Cmd-Attrs
   270  func parseAttribute(meta map[string]string) (map[string]string, error) {
   271  	attribute := make(map[string]string)
   272  	if meta == nil {
   273  		return attribute, nil
   274  	}
   275  
   276  	parseAttrs := func(attrs string) error {
   277  		var err error
   278  		param := strings.Split(attrs, "/")
   279  		for _, val := range param {
   280  			attr := strings.TrimSpace(val)
   281  			if attr == "" {
   282  				err = ErrInvalidFileSystemAttribute
   283  			} else {
   284  				attrVal := strings.Split(attr, ":")
   285  				if len(attrVal) == 2 {
   286  					attribute[strings.TrimSpace(attrVal[0])] = strings.TrimSpace(attrVal[1])
   287  				} else if len(attrVal) == 1 {
   288  					attribute[attrVal[0]] = ""
   289  				} else {
   290  					err = ErrInvalidFileSystemAttribute
   291  				}
   292  			}
   293  		}
   294  		return err
   295  	}
   296  
   297  	if attrs, ok := meta[metadataKey]; ok {
   298  		if err := parseAttrs(attrs); err != nil {
   299  			return attribute, err
   300  		}
   301  	}
   302  
   303  	if attrs, ok := meta[metadataKeyS3Cmd]; ok {
   304  		if err := parseAttrs(attrs); err != nil {
   305  			return attribute, err
   306  		}
   307  	}
   308  
   309  	return attribute, nil
   310  }
   311  
   312  const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
   313  
   314  var reAnsi = regexp.MustCompile(ansi)
   315  
   316  func centerText(s string, w int) string {
   317  	var sb strings.Builder
   318  	textWithoutColor := reAnsi.ReplaceAllString(s, "")
   319  	length := len(textWithoutColor)
   320  	padding := float64(w-length) / 2
   321  	fmt.Fprintf(&sb, "%s", bytes.Repeat([]byte{' '}, int(math.Ceil(padding))))
   322  	fmt.Fprintf(&sb, "%s", s)
   323  	fmt.Fprintf(&sb, "%s", bytes.Repeat([]byte{' '}, int(math.Floor(padding))))
   324  	return sb.String()
   325  }
   326  
   327  func getClient(aliasURL string) *madmin.AdminClient {
   328  	client, err := newAdminClient(aliasURL)
   329  	fatalIf(err, "Unable to initialize admin connection.")
   330  	return client
   331  }
   332  
   333  func httpClient(reqTimeout time.Duration) *http.Client {
   334  	return &http.Client{
   335  		Timeout: reqTimeout,
   336  		Transport: &http.Transport{
   337  			DialContext: (&net.Dialer{
   338  				Timeout: 10 * time.Second,
   339  			}).DialContext,
   340  			Proxy: ieproxy.GetProxyFunc(),
   341  			TLSClientConfig: &tls.Config{
   342  				RootCAs:            globalRootCAs,
   343  				InsecureSkipVerify: globalInsecure,
   344  				// Can't use SSLv3 because of POODLE and BEAST
   345  				// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
   346  				// Can't use TLSv1.1 because of RC4 cipher usage
   347  				MinVersion: tls.VersionTLS12,
   348  			},
   349  			IdleConnTimeout:       90 * time.Second,
   350  			TLSHandshakeTimeout:   10 * time.Second,
   351  			ExpectContinueTimeout: 10 * time.Second,
   352  		},
   353  	}
   354  }
   355  
   356  func getPrometheusToken(hostConfig *aliasConfigV10) (string, error) {
   357  	jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.RegisteredClaims{
   358  		ExpiresAt: jwtgo.NewNumericDate(UTCNow().Add(defaultPrometheusJWTExpiry)),
   359  		Subject:   hostConfig.AccessKey,
   360  		Issuer:    "prometheus",
   361  	})
   362  
   363  	token, e := jwt.SignedString([]byte(hostConfig.SecretKey))
   364  	if e != nil {
   365  		return "", e
   366  	}
   367  	return token, nil
   368  }