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 }