mirror of
https://github.com/minio/mc.git
synced 2025-04-19 21:02:15 +03:00
395 lines
12 KiB
Go
395 lines
12 KiB
Go
// Copyright (c) 2015-2022 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"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/minio/cli"
|
|
"github.com/minio/mc/pkg/probe"
|
|
"github.com/minio/minio-go/v7"
|
|
"github.com/minio/pkg/v3/console"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
const cred = "YellowItalics"
|
|
|
|
var aliasSetFlags = []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "path",
|
|
Value: "auto",
|
|
Usage: "bucket path lookup supported by the server. Valid options are '[auto, on, off]'",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "api",
|
|
Usage: "API signature. Valid options are '[S3v4, S3v2]'",
|
|
},
|
|
}
|
|
|
|
var aliasSetCmd = cli.Command{
|
|
Name: "set",
|
|
ShortName: "s",
|
|
Usage: "set a new alias to configuration file",
|
|
Action: func(cli *cli.Context) error {
|
|
return mainAliasSet(cli, false)
|
|
},
|
|
OnUsageError: onUsageError,
|
|
Before: setGlobalsFromContext,
|
|
Flags: append(aliasSetFlags, globalFlags...),
|
|
HideHelpCommand: true,
|
|
CustomHelpTemplate: `NAME:
|
|
{{.HelpName}} - {{.Usage}}
|
|
|
|
USAGE:
|
|
{{.HelpName}} ALIAS URL ACCESSKEY SECRETKEY
|
|
|
|
FLAGS:
|
|
{{range .VisibleFlags}}{{.}}
|
|
{{end}}
|
|
EXAMPLES:
|
|
1. Add MinIO service under "myminio" alias. For security reasons turn off bash history momentarily.
|
|
{{.DisableHistory}}
|
|
{{.Prompt}} {{.HelpName}} myminio http://localhost:9000 minio minio123
|
|
{{.EnableHistory}}
|
|
2. Add MinIO service under "myminio" alias, to use dns style bucket lookup. For security reasons
|
|
turn off bash history momentarily.
|
|
{{.DisableHistory}}
|
|
{{.Prompt}} {{.HelpName}} myminio http://localhost:9000 minio minio123 --api "s3v4" --path "off"
|
|
{{.EnableHistory}}
|
|
3. Add Amazon S3 storage service under "mys3" alias. For security reasons turn off bash history momentarily.
|
|
{{.DisableHistory}}
|
|
{{.Prompt}} {{.HelpName}} mys3 https://s3.amazonaws.com \
|
|
BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
|
|
{{.EnableHistory}}
|
|
4. Add Amazon S3 storage service under "mys3" alias, prompting for keys.
|
|
{{.Prompt}} {{.HelpName}} mys3 https://s3.amazonaws.com --api "s3v4" --path "off"
|
|
Enter Access Key: BKIKJAA5BMMU2RHO6IBB
|
|
Enter Secret Key: V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
|
|
5. Add Amazon S3 storage service under "mys3" alias using piped keys.
|
|
{{.DisableHistory}}
|
|
{{.Prompt}} echo -e "BKIKJAA5BMMU2RHO6IBB\nV8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12" | \
|
|
{{.HelpName}} mys3 https://s3.amazonaws.com --api "s3v4" --path "off"
|
|
{{.EnableHistory}}
|
|
`,
|
|
}
|
|
|
|
// checkAliasSetSyntax - verifies input arguments to 'alias set'.
|
|
func checkAliasSetSyntax(ctx *cli.Context, accessKey, secretKey string, deprecated bool) {
|
|
args := ctx.Args()
|
|
argsNr := len(args)
|
|
|
|
if argsNr == 0 {
|
|
showCommandHelpAndExit(ctx, 1) // last argument is exit code
|
|
}
|
|
|
|
if argsNr > 4 || argsNr < 2 {
|
|
fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
|
|
"Incorrect number of arguments for alias set command.")
|
|
}
|
|
|
|
alias := cleanAlias(args.Get(0))
|
|
url := args.Get(1)
|
|
api := ctx.String("api")
|
|
path := ctx.String("path")
|
|
bucketLookup := ctx.String("lookup")
|
|
|
|
if !isValidAlias(alias) {
|
|
fatalIf(errInvalidAlias(alias), "Invalid alias.")
|
|
}
|
|
|
|
if !isValidHostURL(url) {
|
|
fatalIf(errInvalidURL(url), "Invalid URL.")
|
|
}
|
|
|
|
if !isValidAccessKey(accessKey) {
|
|
fatalIf(errInvalidArgument().Trace(accessKey),
|
|
"Invalid access key `"+accessKey+"`.")
|
|
}
|
|
|
|
if !isValidSecretKey(secretKey) {
|
|
fatalIf(errInvalidArgument().Trace(secretKey),
|
|
"Invalid secret key `"+secretKey+"`.")
|
|
}
|
|
|
|
if api != "" && !isValidAPI(api) { // Empty value set to default "S3v4".
|
|
fatalIf(errInvalidArgument().Trace(api),
|
|
"Unrecognized API signature. Valid options are `[S3v4, S3v2]`.")
|
|
}
|
|
|
|
if deprecated {
|
|
if !isValidLookup(bucketLookup) {
|
|
fatalIf(errInvalidArgument().Trace(bucketLookup),
|
|
"Unrecognized bucket lookup. Valid options are `[dns,auto, path]`.")
|
|
}
|
|
} else {
|
|
if !isValidPath(path) {
|
|
fatalIf(errInvalidArgument().Trace(path),
|
|
"Unrecognized path value. Valid options are `[auto, on, off]`.")
|
|
}
|
|
}
|
|
}
|
|
|
|
// setAlias - set an alias config.
|
|
func setAlias(alias string, aliasCfgV10 aliasConfigV10) aliasMessage {
|
|
mcCfgV10, err := loadMcConfig()
|
|
fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config `"+mustGetMcConfigPath()+"`.")
|
|
|
|
// Add new host.
|
|
mcCfgV10.Aliases[alias] = aliasCfgV10
|
|
|
|
err = saveMcConfig(mcCfgV10)
|
|
fatalIf(err.Trace(alias), "Unable to update hosts in config version `"+mustGetMcConfigPath()+"`.")
|
|
|
|
return aliasMessage{
|
|
Alias: alias,
|
|
URL: aliasCfgV10.URL,
|
|
AccessKey: aliasCfgV10.AccessKey,
|
|
SecretKey: aliasCfgV10.SecretKey,
|
|
API: aliasCfgV10.API,
|
|
Path: aliasCfgV10.Path,
|
|
}
|
|
}
|
|
|
|
// probeS3Signature - auto probe S3 server signature: issue a Stat call
|
|
// using v4 signature then v2 in case of failure.
|
|
func probeS3Signature(ctx context.Context, accessKey, secretKey, url string, peerCert *x509.Certificate) (string, *probe.Error) {
|
|
probeBucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "probe-bsign-")
|
|
// Test s3 connection for API auto probe
|
|
s3Config := &Config{
|
|
// S3 connection parameters
|
|
Insecure: globalInsecure,
|
|
AccessKey: accessKey,
|
|
SecretKey: secretKey,
|
|
HostURL: urlJoinPath(url, probeBucketName),
|
|
Debug: globalDebug,
|
|
ConnReadDeadline: globalConnReadDeadline,
|
|
ConnWriteDeadline: globalConnWriteDeadline,
|
|
UploadLimit: int64(globalLimitUpload),
|
|
DownloadLimit: int64(globalLimitDownload),
|
|
}
|
|
if peerCert != nil {
|
|
configurePeerCertificate(s3Config, peerCert)
|
|
}
|
|
|
|
probeSignatureType := func(stype string) (string, *probe.Error) {
|
|
s3Config.Signature = stype
|
|
s3Client, err := S3New(s3Config)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err := s3Client.Stat(ctx, StatOptions{}); err != nil {
|
|
e := err.ToGoError()
|
|
if _, ok := e.(BucketDoesNotExist); ok {
|
|
// Bucket doesn't exist, means signature probing worked successfully.
|
|
return stype, nil
|
|
}
|
|
// AccessDenied means Stat() is not allowed but credentials are valid.
|
|
// AccessDenied is only returned when policy doesn't allow HeadBucket
|
|
// operations.
|
|
if minio.ToErrorResponse(err.ToGoError()).Code == "AccessDenied" {
|
|
return stype, nil
|
|
}
|
|
|
|
// For any other errors we fail.
|
|
return "", err.Trace(s3Config.Signature)
|
|
}
|
|
return stype, nil
|
|
}
|
|
|
|
stype, err := probeSignatureType("s3v4")
|
|
if err != nil {
|
|
if stype, err = probeSignatureType("s3v2"); err != nil {
|
|
return "", err.Trace("s3v4", "s3v2")
|
|
}
|
|
return stype, nil
|
|
}
|
|
return stype, nil
|
|
}
|
|
|
|
// BuildS3Config constructs an S3 Config and does
|
|
// signature auto-probe when needed.
|
|
func BuildS3Config(ctx context.Context, alias, url, accessKey, secretKey, api, path string, peerCert *x509.Certificate) (*Config, *probe.Error) {
|
|
s3Config := NewS3Config(alias, url, &aliasConfigV10{
|
|
AccessKey: accessKey,
|
|
SecretKey: secretKey,
|
|
URL: url,
|
|
Path: path,
|
|
})
|
|
|
|
if peerCert != nil {
|
|
configurePeerCertificate(s3Config, peerCert)
|
|
}
|
|
|
|
// If api is provided we do not auto probe signature, this is
|
|
// required in situations when signature type is provided by the user.
|
|
if api != "" {
|
|
s3Config.Signature = api
|
|
return s3Config, nil
|
|
}
|
|
// Probe S3 signature version
|
|
api, err := probeS3Signature(ctx, accessKey, secretKey, url, peerCert)
|
|
if err != nil {
|
|
return nil, err.Trace(url, accessKey, api, path)
|
|
}
|
|
|
|
s3Config.Signature = api
|
|
// Success.
|
|
return s3Config, nil
|
|
}
|
|
|
|
// fetchAliasKeys - returns the user accessKey and secretKey
|
|
func fetchAliasKeys(args cli.Args) (string, string) {
|
|
accessKey := ""
|
|
secretKey := ""
|
|
console.SetColor(cred, color.New(color.FgYellow, color.Italic))
|
|
isTerminal := term.IsTerminal(int(os.Stdin.Fd()))
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
argsNr := len(args)
|
|
|
|
if argsNr == 2 {
|
|
if isTerminal {
|
|
fmt.Printf("%s", console.Colorize(cred, "Enter Access Key: "))
|
|
}
|
|
value, _, _ := reader.ReadLine()
|
|
accessKey = string(value)
|
|
} else {
|
|
accessKey = args.Get(2)
|
|
}
|
|
|
|
if argsNr == 2 || argsNr == 3 {
|
|
if isTerminal {
|
|
fmt.Printf("%s", console.Colorize(cred, "Enter Secret Key: "))
|
|
bytePassword, _ := term.ReadPassword(int(os.Stdin.Fd()))
|
|
fmt.Printf("\n")
|
|
secretKey = string(bytePassword)
|
|
} else {
|
|
value, _, _ := reader.ReadLine()
|
|
secretKey = string(value)
|
|
}
|
|
} else {
|
|
secretKey = args.Get(3)
|
|
}
|
|
|
|
return accessKey, secretKey
|
|
}
|
|
|
|
func mainAliasSet(cli *cli.Context, deprecated bool) error {
|
|
console.SetColor("AliasMessage", color.New(color.FgGreen))
|
|
var (
|
|
args = cli.Args()
|
|
alias = cleanAlias(args.Get(0))
|
|
url = trimTrailingSeparator(args.Get(1))
|
|
api = cli.String("api")
|
|
path = cli.String("path")
|
|
|
|
peerCert *x509.Certificate
|
|
err *probe.Error
|
|
)
|
|
|
|
// Support deprecated lookup flag
|
|
if deprecated {
|
|
lookup := strings.ToLower(strings.TrimSpace(cli.String("lookup")))
|
|
switch lookup {
|
|
case "", "auto":
|
|
path = "auto"
|
|
case "path":
|
|
path = "on"
|
|
case "dns":
|
|
path = "off"
|
|
default:
|
|
}
|
|
}
|
|
|
|
accessKey, secretKey := fetchAliasKeys(args)
|
|
checkAliasSetSyntax(cli, accessKey, secretKey, deprecated)
|
|
|
|
ctx, cancelAliasAdd := context.WithCancel(globalContext)
|
|
defer cancelAliasAdd()
|
|
|
|
if !globalInsecure && !globalJSON && term.IsTerminal(int(os.Stdout.Fd())) {
|
|
peerCert, err = promptTrustSelfSignedCert(ctx, url, alias)
|
|
fatalIf(err.Trace(alias, url, accessKey), "Unable to initialize new alias from the provided credentials.")
|
|
}
|
|
|
|
s3Config, err := BuildS3Config(ctx, alias, url, accessKey, secretKey, api, path, peerCert)
|
|
fatalIf(err.Trace(alias, url, accessKey), "Unable to initialize new alias from the provided credentials.")
|
|
|
|
msg := setAlias(alias, aliasConfigV10{
|
|
URL: s3Config.HostURL,
|
|
AccessKey: s3Config.AccessKey,
|
|
SecretKey: s3Config.SecretKey,
|
|
API: s3Config.Signature,
|
|
Path: path,
|
|
}) // Add an alias with specified credentials.
|
|
|
|
msg.op = "set"
|
|
if deprecated {
|
|
msg.op = "add"
|
|
}
|
|
|
|
printMsg(msg)
|
|
return nil
|
|
}
|
|
|
|
// configurePeerCertificate adds the peer certificate to the
|
|
// TLS root CAs of s3Config. Once configured, any client
|
|
// initialized with this config trusts the given peer certificate.
|
|
func configurePeerCertificate(s3Config *Config, peerCert *x509.Certificate) {
|
|
tr, ok := s3Config.Transport.(*http.Transport)
|
|
if !ok {
|
|
return
|
|
}
|
|
switch {
|
|
case tr == nil:
|
|
if globalRootCAs != nil {
|
|
globalRootCAs.AddCert(peerCert)
|
|
}
|
|
tr = &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: newCustomDialContext(&Config{}),
|
|
DialTLSContext: newCustomDialTLSContext(&tls.Config{RootCAs: globalRootCAs}),
|
|
MaxIdleConnsPerHost: 256,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 10 * time.Second,
|
|
DisableCompression: true,
|
|
}
|
|
case tr.TLSClientConfig == nil || tr.TLSClientConfig.RootCAs == nil:
|
|
if globalRootCAs != nil {
|
|
globalRootCAs.AddCert(peerCert)
|
|
}
|
|
tr.DialTLSContext = newCustomDialTLSContext(&tls.Config{RootCAs: globalRootCAs})
|
|
default:
|
|
tr.TLSClientConfig.RootCAs.AddCert(peerCert)
|
|
tr.DialTLSContext = newCustomDialTLSContext(tr.TLSClientConfig)
|
|
}
|
|
s3Config.Transport = tr
|
|
}
|