mirror of
https://github.com/minio/mc.git
synced 2025-11-10 13:42:32 +03:00
Also adds option to check if the cluster has enough read quorum and if the cluster is taken down for any maintenance. Uses madmin.AnonymousClient for invoking the requests
466 lines
13 KiB
Go
466 lines
13 KiB
Go
// 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 (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mattn/go-ieproxy"
|
|
"github.com/minio/cli"
|
|
"github.com/minio/madmin-go"
|
|
"github.com/minio/minio-go/v7"
|
|
"github.com/minio/minio-go/v7/pkg/encrypt"
|
|
|
|
jwtgo "github.com/golang-jwt/jwt/v4"
|
|
"github.com/minio/mc/pkg/probe"
|
|
"github.com/minio/pkg/console"
|
|
)
|
|
|
|
func isErrIgnored(err *probe.Error) (ignored bool) {
|
|
// For all non critical errors we can continue for the remaining files.
|
|
switch e := err.ToGoError().(type) {
|
|
// Handle these specifically for filesystem related errors.
|
|
case BrokenSymlink, TooManyLevelsSymlink, PathNotFound:
|
|
ignored = true
|
|
// Handle these specifically for object storage related errors.
|
|
case BucketNameEmpty, ObjectMissing, ObjectAlreadyExists:
|
|
ignored = true
|
|
case ObjectAlreadyExistsAsDirectory, BucketDoesNotExist, BucketInvalid:
|
|
ignored = true
|
|
case minio.ErrorResponse:
|
|
ignored = strings.Contains(e.Error(), "The specified key does not exist")
|
|
default:
|
|
ignored = false
|
|
}
|
|
return ignored
|
|
}
|
|
|
|
const (
|
|
letterBytes = "abcdefghijklmnopqrstuvwxyz01234569"
|
|
letterIdxBits = 6 // 6 bits to represent a letter index
|
|
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
|
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
|
)
|
|
|
|
// UTCNow - returns current UTC time.
|
|
func UTCNow() time.Time {
|
|
return time.Now().UTC()
|
|
}
|
|
|
|
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
|
|
// newRandomID generates a random id of regular lower case and uppercase english characters.
|
|
func newRandomID(n int) string {
|
|
rand.Seed(UTCNow().UnixNano())
|
|
sid := make([]rune, n)
|
|
for i := range sid {
|
|
sid[i] = letters[rand.Intn(len(letters))]
|
|
}
|
|
return string(sid)
|
|
}
|
|
|
|
// randString generates random names and prepends them with a known prefix.
|
|
func randString(n int, src rand.Source, prefix string) string {
|
|
b := make([]byte, n)
|
|
// A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
|
|
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
|
if remain == 0 {
|
|
cache, remain = src.Int63(), letterIdxMax
|
|
}
|
|
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
|
b[i] = letterBytes[idx]
|
|
i--
|
|
}
|
|
cache >>= letterIdxBits
|
|
remain--
|
|
}
|
|
return prefix + string(b[0:30-len(prefix)])
|
|
}
|
|
|
|
// printTLSCertInfo prints some fields of the certificates received from the server.
|
|
// Fields will be inspected by the user, so they must be conscise and useful
|
|
func printTLSCertInfo(t *tls.ConnectionState) {
|
|
if globalDebug {
|
|
for _, cert := range t.PeerCertificates {
|
|
console.Debugln("TLS Certificate found: ")
|
|
if len(cert.Issuer.Country) > 0 {
|
|
console.Debugln(" >> Country: " + cert.Issuer.Country[0])
|
|
}
|
|
if len(cert.Issuer.Organization) > 0 {
|
|
console.Debugln(" >> Organization: " + cert.Issuer.Organization[0])
|
|
}
|
|
console.Debugln(" >> Expires: " + cert.NotAfter.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// splitStr splits a string into n parts, empty strings are added
|
|
// if we are not able to reach n elements
|
|
func splitStr(path, sep string, n int) []string {
|
|
splits := strings.SplitN(path, sep, n)
|
|
// Add empty strings if we found elements less than nr
|
|
for i := n - len(splits); i > 0; i-- {
|
|
splits = append(splits, "")
|
|
}
|
|
return splits
|
|
}
|
|
|
|
// NewS3Config simply creates a new Config struct using the passed
|
|
// parameters.
|
|
func NewS3Config(urlStr string, aliasCfg *aliasConfigV10) *Config {
|
|
// We have a valid alias and hostConfig. We populate the
|
|
// credentials from the match found in the config file.
|
|
s3Config := new(Config)
|
|
|
|
s3Config.AppName = filepath.Base(os.Args[0])
|
|
s3Config.AppVersion = ReleaseTag
|
|
s3Config.Debug = globalDebug
|
|
s3Config.Insecure = globalInsecure
|
|
|
|
s3Config.HostURL = urlStr
|
|
if aliasCfg != nil {
|
|
s3Config.AccessKey = aliasCfg.AccessKey
|
|
s3Config.SecretKey = aliasCfg.SecretKey
|
|
s3Config.SessionToken = aliasCfg.SessionToken
|
|
s3Config.Signature = aliasCfg.API
|
|
s3Config.Lookup = getLookupType(aliasCfg.Path)
|
|
}
|
|
return s3Config
|
|
}
|
|
|
|
// lineTrunc - truncates a string to the given maximum length by
|
|
// adding ellipsis in the middle
|
|
func lineTrunc(content string, maxLen int) string {
|
|
runes := []rune(content)
|
|
rlen := len(runes)
|
|
if rlen <= maxLen {
|
|
return content
|
|
}
|
|
halfLen := maxLen / 2
|
|
fstPart := string(runes[0:halfLen])
|
|
sndPart := string(runes[rlen-halfLen:])
|
|
return fstPart + "…" + sndPart
|
|
}
|
|
|
|
// isOlder returns true if the passed object is older than olderRef
|
|
func isOlder(ti time.Time, olderRef string) bool {
|
|
if olderRef == "" {
|
|
return false
|
|
}
|
|
objectAge := time.Since(ti)
|
|
olderThan, e := ParseDuration(olderRef)
|
|
fatalIf(probe.NewError(e), "Unable to parse olderThan=`"+olderRef+"`.")
|
|
return objectAge < time.Duration(olderThan)
|
|
}
|
|
|
|
// isNewer returns true if the passed object is newer than newerRef
|
|
func isNewer(ti time.Time, newerRef string) bool {
|
|
if newerRef == "" {
|
|
return false
|
|
}
|
|
|
|
objectAge := time.Since(ti)
|
|
newerThan, e := ParseDuration(newerRef)
|
|
fatalIf(probe.NewError(e), "Unable to parse newerThan=`"+newerRef+"`.")
|
|
return objectAge >= time.Duration(newerThan)
|
|
}
|
|
|
|
// getLookupType returns the minio.BucketLookupType for lookup
|
|
// option entered on the command line
|
|
func getLookupType(l string) minio.BucketLookupType {
|
|
l = strings.ToLower(l)
|
|
switch l {
|
|
case "off":
|
|
return minio.BucketLookupDNS
|
|
case "on":
|
|
return minio.BucketLookupPath
|
|
}
|
|
return minio.BucketLookupAuto
|
|
}
|
|
|
|
// struct representing object prefix and sse keys association.
|
|
type prefixSSEPair struct {
|
|
Prefix string
|
|
SSE encrypt.ServerSide
|
|
}
|
|
|
|
// parse and validate encryption keys entered on command line
|
|
func parseAndValidateEncryptionKeys(sseKeys string, sse string) (encMap map[string][]prefixSSEPair, err *probe.Error) {
|
|
encMap, err = parseEncryptionKeys(sseKeys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sse != "" {
|
|
for _, prefix := range strings.Split(sse, ",") {
|
|
alias, _ := url2Alias(prefix)
|
|
encMap[alias] = append(encMap[alias], prefixSSEPair{
|
|
Prefix: prefix,
|
|
SSE: encrypt.NewSSE(),
|
|
})
|
|
}
|
|
}
|
|
for alias, ps := range encMap {
|
|
if hostCfg := mustGetHostConfig(alias); hostCfg == nil {
|
|
for _, p := range ps {
|
|
return nil, probe.NewError(errors.New("SSE prefix " + p.Prefix + " has invalid alias"))
|
|
}
|
|
}
|
|
}
|
|
return encMap, nil
|
|
}
|
|
|
|
// parse list of comma separated alias/prefix=sse key values entered on command line and
|
|
// construct a map of alias to prefix and sse pairs.
|
|
func parseEncryptionKeys(sseKeys string) (encMap map[string][]prefixSSEPair, err *probe.Error) {
|
|
encMap = make(map[string][]prefixSSEPair)
|
|
if sseKeys == "" {
|
|
return
|
|
}
|
|
prefix := ""
|
|
index := 0 // start index of prefix
|
|
vs := 0 // start index of sse-c key
|
|
sseKeyLen := 32
|
|
delim := 1
|
|
k := len(sseKeys)
|
|
for index < k {
|
|
i := strings.Index(sseKeys[index:], "=")
|
|
if i == -1 {
|
|
return nil, probe.NewError(errors.New("SSE-C prefix should be of the form prefix1=key1,... "))
|
|
}
|
|
prefix = sseKeys[index : index+i]
|
|
alias, _ := url2Alias(prefix)
|
|
vs = i + 1 + index
|
|
if vs+32 > k {
|
|
return nil, probe.NewError(errors.New("SSE-C key should be 32 bytes long"))
|
|
}
|
|
if (vs+sseKeyLen < k) && sseKeys[vs+sseKeyLen] != ',' {
|
|
return nil, probe.NewError(errors.New("SSE-C prefix=secret should be delimited by , and secret should be 32 bytes long"))
|
|
}
|
|
sseKey := sseKeys[vs : vs+sseKeyLen]
|
|
if _, ok := encMap[alias]; !ok {
|
|
encMap[alias] = make([]prefixSSEPair, 0)
|
|
}
|
|
sse, e := encrypt.NewSSEC([]byte(sseKey))
|
|
if e != nil {
|
|
return nil, probe.NewError(e)
|
|
}
|
|
encMap[alias] = append(encMap[alias], prefixSSEPair{
|
|
Prefix: prefix,
|
|
SSE: sse,
|
|
})
|
|
// advance index sseKeyLen + delim bytes for the next key start
|
|
index = vs + sseKeyLen + delim
|
|
}
|
|
|
|
// Sort encryption keys in descending order of prefix length
|
|
for _, encKeys := range encMap {
|
|
sort.Sort(byPrefixLength(encKeys))
|
|
}
|
|
|
|
// Success.
|
|
return encMap, nil
|
|
}
|
|
|
|
// byPrefixLength implements sort.Interface.
|
|
type byPrefixLength []prefixSSEPair
|
|
|
|
func (p byPrefixLength) Len() int { return len(p) }
|
|
func (p byPrefixLength) Less(i, j int) bool {
|
|
return len(p[i].Prefix) > len(p[j].Prefix)
|
|
}
|
|
func (p byPrefixLength) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|
|
|
// get SSE Key if object prefix matches with given resource.
|
|
func getSSE(resource string, encKeys []prefixSSEPair) encrypt.ServerSide {
|
|
for _, k := range encKeys {
|
|
if strings.HasPrefix(resource, k.Prefix) {
|
|
return k.SSE
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Return true if target url is a part of a source url such as:
|
|
// alias/bucket/ and alias/bucket/dir/, however
|
|
func isURLContains(srcURL, tgtURL, sep string) bool {
|
|
// Add a separator to source url if not found
|
|
if !strings.HasSuffix(srcURL, sep) {
|
|
srcURL += sep
|
|
}
|
|
if !strings.HasSuffix(tgtURL, sep) {
|
|
tgtURL += sep
|
|
}
|
|
// Check if we are going to copy a directory into itself
|
|
if strings.HasPrefix(tgtURL, srcURL) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ErrInvalidFileSystemAttribute reflects invalid fily system attribute
|
|
var ErrInvalidFileSystemAttribute = errors.New("Error in parsing file system attribute")
|
|
|
|
func parseAtimeMtime(attr map[string]string) (atime, mtime time.Time, err *probe.Error) {
|
|
if val, ok := attr["atime"]; ok {
|
|
vals := strings.SplitN(val, "#", 2)
|
|
atim, e := strconv.ParseInt(vals[0], 10, 64)
|
|
if e != nil {
|
|
return atime, mtime, probe.NewError(e)
|
|
}
|
|
var atimnsec int64
|
|
if len(vals) == 2 {
|
|
atimnsec, e = strconv.ParseInt(vals[1], 10, 64)
|
|
if e != nil {
|
|
return atime, mtime, probe.NewError(e)
|
|
}
|
|
}
|
|
atime = time.Unix(atim, atimnsec)
|
|
}
|
|
|
|
if val, ok := attr["mtime"]; ok {
|
|
vals := strings.SplitN(val, "#", 2)
|
|
mtim, e := strconv.ParseInt(vals[0], 10, 64)
|
|
if e != nil {
|
|
return atime, mtime, probe.NewError(e)
|
|
}
|
|
var mtimnsec int64
|
|
if len(vals) == 2 {
|
|
mtimnsec, e = strconv.ParseInt(vals[1], 10, 64)
|
|
if e != nil {
|
|
return atime, mtime, probe.NewError(e)
|
|
}
|
|
}
|
|
mtime = time.Unix(mtim, mtimnsec)
|
|
}
|
|
return atime, mtime, nil
|
|
}
|
|
|
|
// Returns a map by parsing the value of X-Amz-Meta-Mc-Attrs/X-Amz-Meta-s3Cmd-Attrs
|
|
func parseAttribute(meta map[string]string) (map[string]string, error) {
|
|
attribute := make(map[string]string)
|
|
if meta == nil {
|
|
return attribute, nil
|
|
}
|
|
|
|
parseAttrs := func(attrs string) error {
|
|
var err error
|
|
param := strings.Split(attrs, "/")
|
|
for _, val := range param {
|
|
attr := strings.TrimSpace(val)
|
|
if attr == "" {
|
|
err = ErrInvalidFileSystemAttribute
|
|
} else {
|
|
attrVal := strings.Split(attr, ":")
|
|
if len(attrVal) == 2 {
|
|
attribute[strings.TrimSpace(attrVal[0])] = strings.TrimSpace(attrVal[1])
|
|
} else if len(attrVal) == 1 {
|
|
attribute[attrVal[0]] = ""
|
|
} else {
|
|
err = ErrInvalidFileSystemAttribute
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
if attrs, ok := meta[metadataKey]; ok {
|
|
if err := parseAttrs(attrs); err != nil {
|
|
return attribute, err
|
|
}
|
|
}
|
|
|
|
if attrs, ok := meta[metadataKeyS3Cmd]; ok {
|
|
if err := parseAttrs(attrs); err != nil {
|
|
return attribute, err
|
|
}
|
|
}
|
|
|
|
return attribute, nil
|
|
}
|
|
|
|
const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
|
|
|
|
var reAnsi = regexp.MustCompile(ansi)
|
|
|
|
func centerText(s string, w int) string {
|
|
var sb strings.Builder
|
|
textWithoutColor := reAnsi.ReplaceAllString(s, "")
|
|
length := len(textWithoutColor)
|
|
padding := float64(w-length) / 2
|
|
fmt.Fprintf(&sb, "%s", bytes.Repeat([]byte{' '}, int(math.Ceil(padding))))
|
|
fmt.Fprintf(&sb, "%s", s)
|
|
fmt.Fprintf(&sb, "%s", bytes.Repeat([]byte{' '}, int(math.Floor(padding))))
|
|
return sb.String()
|
|
}
|
|
|
|
func getAliasAndBucket(ctx *cli.Context) (string, string) {
|
|
args := ctx.Args()
|
|
aliasedURL := args.Get(0)
|
|
aliasedURL = filepath.Clean(aliasedURL)
|
|
return url2Alias(aliasedURL)
|
|
}
|
|
|
|
func getClient(aliasURL string) *madmin.AdminClient {
|
|
client, err := newAdminClient(aliasURL)
|
|
fatalIf(err, "Unable to initialize admin connection.")
|
|
return client
|
|
}
|
|
|
|
func httpClient(timeout time.Duration) *http.Client {
|
|
return &http.Client{
|
|
Timeout: timeout,
|
|
Transport: &http.Transport{
|
|
Proxy: ieproxy.GetProxyFunc(),
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: globalRootCAs,
|
|
// Can't use SSLv3 because of POODLE and BEAST
|
|
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
|
// Can't use TLSv1.1 because of RC4 cipher usage
|
|
MinVersion: tls.VersionTLS12,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func getPrometheusToken(hostConfig *aliasConfigV10) (string, error) {
|
|
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{
|
|
ExpiresAt: UTCNow().Add(defaultPrometheusJWTExpiry).Unix(),
|
|
Subject: hostConfig.AccessKey,
|
|
Issuer: "prometheus",
|
|
})
|
|
|
|
token, e := jwt.SignedString([]byte(hostConfig.SecretKey))
|
|
if e != nil {
|
|
return "", e
|
|
}
|
|
return token, nil
|
|
}
|