diff --git a/cmd/admin-trace.go b/cmd/admin-trace.go index 988ab06bb7..042d7ed607 100644 --- a/cmd/admin-trace.go +++ b/cmd/admin-trace.go @@ -900,6 +900,7 @@ type statItem struct { CallStatsCount int `json:"callStatsCount,omitempty"` CallStats callStats `json:"callStats,omitempty"` TTFB time.Duration `json:"ttfb,omitempty"` + MaxTTFB time.Duration `json:"maxTTFB,omitempty"` MaxDur time.Duration `json:"maxDuration"` MinDur time.Duration `json:"minDuration"` } @@ -939,6 +940,9 @@ func (s *statTrace) add(t madmin.ServiceTraceInfo) { if got.MaxDur < t.Trace.Duration { got.MaxDur = t.Trace.Duration } + if got.MaxTTFB < t.Trace.HTTP.CallStats.TimeToFirstByte { + got.MaxTTFB = t.Trace.HTTP.CallStats.TimeToFirstByte + } if got.MinDur <= 0 { got.MinDur = t.Trace.Duration } @@ -1078,9 +1082,10 @@ func (m *traceStatsUI) View() string { console.Colorize("metrics-top-title", "Count"), console.Colorize("metrics-top-title", "RPM"), console.Colorize("metrics-top-title", "Avg Time"), - console.Colorize("metrics-top-title", "TTFB Time"), console.Colorize("metrics-top-title", "Min Time"), console.Colorize("metrics-top-title", "Max Time"), + console.Colorize("metrics-top-title", "Avg TTFB"), + console.Colorize("metrics-top-title", "Max TTFB"), console.Colorize("metrics-top-title", "Errors"), console.Colorize("metrics-top-title", "RX Avg"), console.Colorize("metrics-top-title", "TX Avg"), @@ -1128,9 +1133,10 @@ func (m *traceStatsUI) View() string { console.Colorize("metrics-number-secondary", fmt.Sprintf("(%0.1f%%)", float64(v.Count)/float64(totalCnt)*100)), console.Colorize("metrics-number", fmt.Sprintf("%0.1f", float64(v.Count)/dur.Minutes())), console.Colorize(avgColor, fmt.Sprintf("%v", avg.Round(time.Microsecond))), - console.Colorize(avgColor, fmt.Sprintf("%v", avgTTFB.Round(time.Microsecond))), console.Colorize(minColor, v.MinDur), console.Colorize(maxColor, v.MaxDur), + console.Colorize(avgColor, fmt.Sprintf("%v", avgTTFB.Round(time.Microsecond))), + console.Colorize(maxColor, v.MaxTTFB), errs, rx, tx, diff --git a/cmd/client-admin.go b/cmd/client-admin.go index 6f62413952..eb3b9be8a4 100644 --- a/cmd/client-admin.go +++ b/cmd/client-admin.go @@ -24,13 +24,18 @@ import ( "net" "net/http" "net/url" + "os" "sync" "time" "github.com/klauspost/compress/gzhttp" + + "github.com/minio/pkg/v2/env" + "github.com/mattn/go-ieproxy" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/httptracer" + "github.com/minio/mc/pkg/limiter" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/credentials" ) @@ -67,8 +72,97 @@ func NewAdminFactory() func(config *Config) (*madmin.AdminClient, *probe.Error) var api *madmin.AdminClient var found bool if api, found = clientCache[confSum]; !found { + + var transport http.RoundTripper + + if config.Transport != nil { + transport = config.Transport + } else { + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: newCustomDialContext(config), + MaxIdleConnsPerHost: 1024, + WriteBufferSize: 32 << 10, // 32KiB moving up from 4KiB default + ReadBufferSize: 32 << 10, // 32KiB moving up from 4KiB default + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 10 * time.Second, + DisableCompression: true, + // Set this value so that the underlying transport round-tripper + // doesn't try to auto decode the body of objects with + // content-encoding set to `gzip`. + // + // Refer: + // https://golang.org/src/net/http/transport.go?h=roundTrip#L1843 + } + if useTLS { + // Keep TLS config. + tlsConfig := &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, + } + if config.Insecure { + tlsConfig.InsecureSkipVerify = true + } + tr.TLSClientConfig = tlsConfig + + // Because we create a custom TLSClientConfig, we have to opt-in to HTTP/2. + // See https://github.com/golang/go/issues/14275 + // + // TODO: Enable http2.0 when upstream issues related to HTTP/2 are fixed. + // + // if e = http2.ConfigureTransport(tr); e != nil { + // return nil, probe.NewError(e) + // } + } + transport = tr + } + + transport = limiter.New(config.UploadLimit, config.DownloadLimit, transport) + + if config.Debug { + transport = httptracer.GetNewTraceTransport(newTraceV4(), transport) + } + + transport = gzhttp.Transport(transport) + + var credsChain []credentials.Provider + + // if an STS endpoint is set, we will add that to the chain + if stsEndpoint := env.Get("MC_STS_ENDPOINT", ""); stsEndpoint != "" { + // set AWS_WEB_IDENTITY_TOKEN_FILE is MC_WEB_IDENTITY_TOKEN_FILE is set + if val := env.Get("MC_WEB_IDENTITY_TOKEN_FILE", ""); val != "" { + os.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", val) + } + + stsEndpointURL, err := url.Parse(stsEndpoint) + if err != nil { + return nil, probe.NewError(fmt.Errorf("Error parsing sts endpoint: %v", err)) + } + credsSts := &credentials.IAM{ + Client: &http.Client{ + Transport: transport, + }, + Endpoint: stsEndpointURL.String(), + } + credsChain = append(credsChain, credsSts) + } + + // V4 Credentials + credsV4 := &credentials.Static{ + Value: credentials.Value{ + AccessKeyID: config.AccessKey, + SecretAccessKey: config.SecretKey, + SessionToken: config.SessionToken, + SignerType: credentials.SignatureV4, + }, + } + credsChain = append(credsChain, credsV4) // Admin API only supports signature v4. - creds := credentials.NewStaticV4(config.AccessKey, config.SecretKey, config.SessionToken) + creds := credentials.NewChainCredentials(credsChain) // Not found. Instantiate a new MinIO var e error @@ -80,34 +174,6 @@ func NewAdminFactory() func(config *Config) (*madmin.AdminClient, *probe.Error) return nil, probe.NewError(e) } - // Keep TLS config. - tlsConfig := &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, - } - if config.Insecure { - tlsConfig.InsecureSkipVerify = true - } - - var transport http.RoundTripper = &http.Transport{ - Proxy: ieproxy.GetProxyFunc(), - DialContext: newCustomDialContext(config), - MaxIdleConnsPerHost: 256, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 10 * time.Second, - TLSClientConfig: tlsConfig, - DisableCompression: true, - } - transport = gzhttp.Transport(transport) - - if config.Debug { - transport = httptracer.GetNewTraceTransport(newTraceV4(), transport) - } - // Set custom transport. api.SetCustomTransport(transport) diff --git a/cmd/client-admin_test.go b/cmd/client-admin_test.go new file mode 100644 index 0000000000..46f6ce6673 --- /dev/null +++ b/cmd/client-admin_test.go @@ -0,0 +1,127 @@ +// Copyright (c) 2015-2023 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 . + +package cmd + +import ( + "bytes" + "context" + "io" + "log" + "net/http" + "net/http/httptest" + "os" + "strconv" + + checkv1 "gopkg.in/check.v1" +) + +type adminPolicyHandler struct { + endpoint string + name string + policy []byte +} + +func (h adminPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if ak := r.Header.Get("Authorization"); len(ak) == 0 { + w.WriteHeader(http.StatusForbidden) + return + } + switch { + case r.Method == "PUT": + length, e := strconv.Atoi(r.Header.Get("Content-Length")) + if e != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + var buffer bytes.Buffer + if _, e = io.CopyN(&buffer, r.Body, int64(length)); e != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if len(h.policy) != buffer.Len() { + w.WriteHeader(http.StatusBadRequest) + return + } + + w.Header().Set("Content-Length", "0") + w.WriteHeader(http.StatusOK) + + default: + w.WriteHeader(http.StatusForbidden) + } +} + +func (s *TestSuite) TestAdminSTSOperation(c *checkv1.C) { + sts := stsHandler{ + endpoint: "/", + jwt: []byte("eyJhbGciOiJSUzI1NiIsImtpZCI6Inc0dFNjMEc5Tk0wQWhGaWJYaWIzbkpRZkRKeDc1dURRTUVpOTNvTHJ0OWcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxMTg3NzEwLCJpYXQiOjE2OTk2NTE3MTAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJtaW5pby10ZW5hbnQtMSIsInBvZCI6eyJuYW1lIjoic2V0dXAtYnVja2V0LXQ4eGdjIiwidWlkIjoiNjZhYjlkZWItNzkwMC00YTFlLTgzMDgtMTkwODIwZmQ3NDY5In0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJtYy1qb2Itc2EiLCJ1aWQiOiI3OTc4NzJjZC1kMjkwLTRlM2EtYjYyMC00ZGFkYzZhNzUyMTYifSwid2FybmFmdGVyIjoxNjk5NjU1MzE3fSwibmJmIjoxNjk5NjUxNzEwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6bWluaW8tdGVuYW50LTE6bWMtam9iLXNhIn0.rY7dpAh8GBTViH9Ges7tRhgyihdFWEN0DwXchelmZg58VOI526S-YfbCqrxksTs8Iu0fp1rmk1cUj7FGDh3AOv2RphHjoWci1802zKkHgH0iOEbKMp3jHXwfyHda8CyrSCPycGzClueCf1ae91wd_0lgK9lOR1qqY1HuDeXqSEAUIGrfh1VcP2n95Zc07EY-Uh3XjJE4drtgusACEK5n3P3WtN9s0m0GomEGQzF5ZJczxLGpHBKMQ5VDhMksVKdBAsx9xHgSx84aUhKQViYilAL-8PRj-RZA9s_IpEymAh5R37dKzAO8Fqq0nG7fVbH_ifzw3xhHiG92BhHldBDqEQ"), + } + + tmpfile, errFs := os.CreateTemp("", "jwt") + if errFs != nil { + log.Fatal(errFs) + } + defer os.Remove(tmpfile.Name()) // clean up + + if _, errFs := tmpfile.Write(sts.jwt); errFs != nil { + log.Fatal(errFs) + } + if errFs := tmpfile.Close(); errFs != nil { + log.Fatal(errFs) + } + + stsServer := httptest.NewServer(sts) + defer stsServer.Close() + os.Setenv("MC_STS_ENDPOINT", stsServer.URL+sts.endpoint) + os.Setenv("MC_WEB_IDENTITY_TOKEN_FILE", tmpfile.Name()) + handler := adminPolicyHandler{ + endpoint: "/minio/admin/v3/add-canned-policy?name=", + name: "test", + policy: []byte(` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:aws:s3:::test-bucket", + "arn:aws:s3:::test-bucket/*" + ] + } + ] + +}`), + } + server := httptest.NewServer(handler) + defer server.Close() + + conf := new(Config) + conf.Debug = true + conf.Insecure = true + conf.HostURL = server.URL + handler.endpoint + handler.name + s3c, err := s3AdminNew(conf) + c.Assert(err, checkv1.IsNil) + + policyErr := s3c.AddCannedPolicy(context.Background(), handler.name, handler.policy) + c.Assert(policyErr, checkv1.IsNil) +} diff --git a/cmd/client-s3.go b/cmd/client-s3.go index 8e79349c0b..1db1dc9c2d 100644 --- a/cmd/client-s3.go +++ b/cmd/client-s3.go @@ -132,13 +132,19 @@ func newFactory() func(config *Config) (Client, *probe.Error) { useTLS = false } + // Save if target supports virtual host style. + hostName := targetURL.Host + + // Generate a hash out of s3Conf. + confHash := fnv.New32a() + confHash.Write([]byte(hostName + config.AccessKey + config.SecretKey + config.SessionToken)) + confSum := confHash.Sum32() + // Instantiate s3 s3Clnt := &S3Client{} // Save the target URL. s3Clnt.targetURL = targetURL - // Save if target supports virtual host style. - hostName := targetURL.Host s3Clnt.virtualStyle = isVirtualHostStyle(hostName, config.Lookup) isS3AcceleratedEndpoint := isAmazonAccelerated(hostName) @@ -149,11 +155,6 @@ func newFactory() func(config *Config) (Client, *probe.Error) { } } - // Generate a hash out of s3Conf. - confHash := fnv.New32a() - confHash.Write([]byte(hostName + config.AccessKey + config.SecretKey + config.SessionToken)) - confSum := confHash.Sum32() - // Lookup previous cache by hash. mutex.Lock() defer mutex.Unlock() diff --git a/cmd/client-s3_test.go b/cmd/client-s3_test.go index 8fed981291..79065e024f 100644 --- a/cmd/client-s3_test.go +++ b/cmd/client-s3_test.go @@ -22,8 +22,10 @@ import ( "bytes" "context" "io" + "log" "net/http" "net/http/httptest" + "os" "strconv" minio "github.com/minio/minio-go/v7" @@ -80,6 +82,11 @@ type objectHandler struct { } func (h objectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if ak := r.Header.Get("Authorization"); len(ak) == 0 { + w.WriteHeader(http.StatusForbidden) + return + } + switch { case r.Method == "PUT": // Handler for PUT object request. @@ -156,6 +163,38 @@ func (h objectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +type stsHandler struct { + endpoint string + jwt []byte +} + +func (h stsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := ParseForm(r); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + switch { + case r.Method == "POST": + token := r.Form.Get("WebIdentityToken") + if token == string(h.jwt) { + response := []byte("7NL5BR739GUQ0ZOD4JNBA2mxZSxPnHNhSduedUHczsXZpVSSssOLpDruUmTV0001-01-01T00:00:00ZeyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI3Tkw1QlI3MzlHVVEwWk9ENEpOQiIsImV4cCI6MTY5OTYwMzMwNiwicGFyZW50IjoibWluaW8iLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaVlXUnRhVzQ2S2lKZGZTeDdJa1ZtWm1WamRDSTZJa0ZzYkc5M0lpd2lRV04wYVc5dUlqcGJJbXR0Y3pvcUlsMTlMSHNpUldabVpXTjBJam9pUVd4c2IzY2lMQ0pCWTNScGIyNGlPbHNpY3pNNktpSmRMQ0pTWlhOdmRYSmpaU0k2V3lKaGNtNDZZWGR6T25Nek9qbzZLaUpkZlYxOSJ9.uuE_x7PO8QoPfUk9KzUELoAqxihIknZAvJLl5aYJjwpSjJYFTPLp6EvuyJX2hc18s9HzeiJ-vU0dPzsy50dXmg") + w.Header().Set("Content-Length", strconv.Itoa(len(response))) + w.Header().Set("Content-Type", "application/xml") + w.Header().Set("Server", "MinIO") + w.Write(response) + w.WriteHeader(http.StatusOK) + return + } else { + response := []byte("AccessDeniedAccess denied: Invalid Token") + w.Header().Set("Content-Length", strconv.Itoa(len(response))) + w.Header().Set("Content-Type", "application/xml") + w.Write(response) + w.WriteHeader(http.StatusForbidden) + return + } + } +} + // Test bucket operations. func (s *TestSuite) TestBucketOperations(c *checkv1.C) { bucket := bucketHandler{ @@ -240,6 +279,52 @@ func (s *TestSuite) TestObjectOperations(c *checkv1.C) { } } +func (s *TestSuite) TestSTSOperation(c *checkv1.C) { + sts := stsHandler{ + endpoint: "/", + jwt: []byte("eyJhbGciOiJSUzI1NiIsImtpZCI6Inc0dFNjMEc5Tk0wQWhGaWJYaWIzbkpRZkRKeDc1dURRTUVpOTNvTHJ0OWcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxMTIyNjg0LCJpYXQiOjE2OTk1ODY2ODQsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJtaW5pby10ZW5hbnQtMSIsInBvZCI6eyJuYW1lIjoic2V0dXAtYnVja2V0LXJ4aHhiIiwidWlkIjoiNmNhMzhjMmItYTdkMC00M2Y0LWE0NjMtZjdlNjU4MGUyZDdiIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJtYy1qb2Itc2EiLCJ1aWQiOiI3OTc4NzJjZC1kMjkwLTRlM2EtYjYyMC00ZGFkYzZhNzUyMTYifSwid2FybmFmdGVyIjoxNjk5NTkwMjkxfSwibmJmIjoxNjk5NTg2Njg0LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6bWluaW8tdGVuYW50LTE6bWMtam9iLXNhIn0.fBJckmoQFyJ9bUgKZv6jzBESd9ccX_HFPPBZ17Gz_CsQ5wXrMqnvoMs1mcv6QKWsDsvSnWnw_tcW0cjvVkXb2mKmioKLzqV4ihGbiWzwk2e1xDohn8fizdQkf64bXpncjGdEGv8oi9A4300jfLMfg53POriMyEAQMeIDKPOI9qx913xjGni2w2H49mjLfnFnRaj9osvy17425dNIrMC6GDFq3rcq6Z_cdDmL18Jwsjy1xDsAhUzmOclr-VI3AeSnuD4fbf6jhbKE14qVUjLmIBf__B5NhESiaFNwxFYjonZyi357Nx93CD1wai28tNRSODx7BiPHLxk8SyzY0CP0sQ"), + } + + tmpfile, errFs := os.CreateTemp("", "jwt") + if errFs != nil { + log.Fatal(errFs) + } + defer os.Remove(tmpfile.Name()) // clean up + + if _, errFs := tmpfile.Write(sts.jwt); errFs != nil { + log.Fatal(errFs) + } + if errFs := tmpfile.Close(); errFs != nil { + log.Fatal(errFs) + } + + stsServer := httptest.NewServer(sts) + defer stsServer.Close() + os.Setenv("MC_STS_ENDPOINT", stsServer.URL+sts.endpoint) + os.Setenv("MC_WEB_IDENTITY_TOKEN_FILE", tmpfile.Name()) + object := objectHandler{ + resource: "/bucket/object", + data: []byte("Hello, World"), + } + server := httptest.NewServer(object) + defer server.Close() + + conf := new(Config) + conf.HostURL = server.URL + object.resource + s3c, err := S3New(conf) + c.Assert(err, checkv1.IsNil) + + var reader io.Reader + reader = bytes.NewReader(object.data) + n, err := s3c.Put(context.Background(), reader, int64(len(object.data)), nil, PutOptions{ + metadata: map[string]string{ + "Content-Type": "application/octet-stream", + }, + }) + c.Assert(err, checkv1.IsNil) + c.Assert(n, checkv1.Equals, int64(len(object.data))) +} + var testSelectCompressionTypeCases = []struct { opts SelectObjectOpts object string diff --git a/cmd/common-methods.go b/cmd/common-methods.go index f7d6a124d6..1d4dfe563b 100644 --- a/cmd/common-methods.go +++ b/cmd/common-methods.go @@ -654,3 +654,16 @@ func newClient(aliasedURL string) (Client, *probe.Error) { } return newClientFromAlias(alias, urlStrFull) } + +// ParseForm parses a http.Request form and populates the array +func ParseForm(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + for k, v := range r.PostForm { + if _, ok := r.Form[k]; !ok { + r.Form[k] = v + } + } + return nil +} diff --git a/cmd/idp-ldap-accesskey-list.go b/cmd/idp-ldap-accesskey-list.go index 14245804fa..be1b3f8971 100644 --- a/cmd/idp-ldap-accesskey-list.go +++ b/cmd/idp-ldap-accesskey-list.go @@ -64,7 +64,7 @@ EXAMPLES: 1. Get list of all users and associated access keys in local server (if admin) {{.Prompt}} {{.HelpName}} local/ 2. Get list of users in local server (if admin) - {{.Prompt}} {{.HelpName}} local/ --users + {{.Prompt}} {{.HelpName}} local/ --users-only 3. Get list of all users and associated temporary access keys in play server (if admin) {{.Prompt}} {{.HelpName}} play/ --temp-only 4. Get list of access keys associated with user 'bobfisher' diff --git a/cmd/ilm-rule-add.go b/cmd/ilm-rule-add.go index 823e7c2266..57ae31bc37 100644 --- a/cmd/ilm-rule-add.go +++ b/cmd/ilm-rule-add.go @@ -156,8 +156,9 @@ var ilmAddFlags = []cli.Flag{ Hidden: true, }, cli.IntFlag{ - Name: "noncurrent-transition-newer", - Usage: "number of noncurrent versions to retain in hot tier", + Name: "noncurrent-transition-newer", + Usage: "number of noncurrent versions to retain in hot tier", + Hidden: true, }, cli.StringFlag{ Name: "noncurrentversion-transition-storage-class",