storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/madmin/api.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2016-2020 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package madmin
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/sha256"
    23  	"encoding/hex"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"math/rand"
    29  	"net/http"
    30  	"net/http/cookiejar"
    31  	"net/http/httputil"
    32  	"net/url"
    33  	"os"
    34  	"regexp"
    35  	"runtime"
    36  	"strings"
    37  	"syscall"
    38  	"time"
    39  
    40  	"github.com/minio/minio-go/v7/pkg/credentials"
    41  	"github.com/minio/minio-go/v7/pkg/s3utils"
    42  	"github.com/minio/minio-go/v7/pkg/signer"
    43  	"golang.org/x/net/publicsuffix"
    44  )
    45  
    46  // AdminClient implements Amazon S3 compatible methods.
    47  type AdminClient struct {
    48  	///  Standard options.
    49  
    50  	// Parsed endpoint url provided by the user.
    51  	endpointURL *url.URL
    52  
    53  	// Holds various credential providers.
    54  	credsProvider *credentials.Credentials
    55  
    56  	// User supplied.
    57  	appInfo struct {
    58  		appName    string
    59  		appVersion string
    60  	}
    61  
    62  	// Indicate whether we are using https or not
    63  	secure bool
    64  
    65  	// Needs allocation.
    66  	httpClient *http.Client
    67  
    68  	random *rand.Rand
    69  
    70  	// Advanced functionality.
    71  	isTraceEnabled bool
    72  	traceOutput    io.Writer
    73  }
    74  
    75  // Global constants.
    76  const (
    77  	libraryName    = "madmin-go"
    78  	libraryVersion = "0.0.1"
    79  
    80  	libraryAdminURLPrefix = "/minio/admin"
    81  )
    82  
    83  // User Agent should always following the below style.
    84  // Please open an issue to discuss any new changes here.
    85  //
    86  //       MinIO (OS; ARCH) LIB/VER APP/VER
    87  const (
    88  	libraryUserAgentPrefix = "MinIO (" + runtime.GOOS + "; " + runtime.GOARCH + ") "
    89  	libraryUserAgent       = libraryUserAgentPrefix + libraryName + "/" + libraryVersion
    90  )
    91  
    92  // Options for New method
    93  type Options struct {
    94  	Creds  *credentials.Credentials
    95  	Secure bool
    96  	// Add future fields here
    97  }
    98  
    99  // New - instantiate minio admin client
   100  func New(endpoint string, accessKeyID, secretAccessKey string, secure bool) (*AdminClient, error) {
   101  	creds := credentials.NewStaticV4(accessKeyID, secretAccessKey, "")
   102  
   103  	clnt, err := privateNew(endpoint, creds, secure)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	return clnt, nil
   108  }
   109  
   110  // NewWithOptions - instantiate minio admin client with options.
   111  func NewWithOptions(endpoint string, opts *Options) (*AdminClient, error) {
   112  	clnt, err := privateNew(endpoint, opts.Creds, opts.Secure)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	return clnt, nil
   117  }
   118  
   119  func privateNew(endpoint string, creds *credentials.Credentials, secure bool) (*AdminClient, error) {
   120  	// Initialize cookies to preserve server sent cookies if any and replay
   121  	// them upon each request.
   122  	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	// construct endpoint.
   128  	endpointURL, err := getEndpointURL(endpoint, secure)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	clnt := new(AdminClient)
   134  
   135  	// Save the credentials.
   136  	clnt.credsProvider = creds
   137  
   138  	// Remember whether we are using https or not
   139  	clnt.secure = secure
   140  
   141  	// Save endpoint URL, user agent for future uses.
   142  	clnt.endpointURL = endpointURL
   143  
   144  	// Instantiate http client and bucket location cache.
   145  	clnt.httpClient = &http.Client{
   146  		Jar:       jar,
   147  		Transport: DefaultTransport(secure),
   148  	}
   149  
   150  	// Add locked pseudo-random number generator.
   151  	clnt.random = rand.New(&lockedRandSource{src: rand.NewSource(time.Now().UTC().UnixNano())})
   152  
   153  	// Return.
   154  	return clnt, nil
   155  }
   156  
   157  // SetAppInfo - add application details to user agent.
   158  func (adm *AdminClient) SetAppInfo(appName string, appVersion string) {
   159  	// if app name and version is not set, we do not a new user
   160  	// agent.
   161  	if appName != "" && appVersion != "" {
   162  		adm.appInfo.appName = appName
   163  		adm.appInfo.appVersion = appVersion
   164  	}
   165  }
   166  
   167  // SetCustomTransport - set new custom transport.
   168  func (adm *AdminClient) SetCustomTransport(customHTTPTransport http.RoundTripper) {
   169  	// Set this to override default transport
   170  	// ``http.DefaultTransport``.
   171  	//
   172  	// This transport is usually needed for debugging OR to add your
   173  	// own custom TLS certificates on the client transport, for custom
   174  	// CA's and certs which are not part of standard certificate
   175  	// authority follow this example :-
   176  	//
   177  	//   tr := &http.Transport{
   178  	//           TLSClientConfig:    &tls.Config{RootCAs: pool},
   179  	//           DisableCompression: true,
   180  	//   }
   181  	//   api.SetTransport(tr)
   182  	//
   183  	if adm.httpClient != nil {
   184  		adm.httpClient.Transport = customHTTPTransport
   185  	}
   186  }
   187  
   188  // TraceOn - enable HTTP tracing.
   189  func (adm *AdminClient) TraceOn(outputStream io.Writer) {
   190  	// if outputStream is nil then default to os.Stdout.
   191  	if outputStream == nil {
   192  		outputStream = os.Stdout
   193  	}
   194  	// Sets a new output stream.
   195  	adm.traceOutput = outputStream
   196  
   197  	// Enable tracing.
   198  	adm.isTraceEnabled = true
   199  }
   200  
   201  // TraceOff - disable HTTP tracing.
   202  func (adm *AdminClient) TraceOff() {
   203  	// Disable tracing.
   204  	adm.isTraceEnabled = false
   205  }
   206  
   207  // requestMetadata - is container for all the values to make a
   208  // request.
   209  type requestData struct {
   210  	customHeaders http.Header
   211  	queryValues   url.Values
   212  	relPath       string // URL path relative to admin API base endpoint
   213  	content       []byte
   214  }
   215  
   216  // Filter out signature value from Authorization header.
   217  func (adm AdminClient) filterSignature(req *http.Request) {
   218  	/// Signature V4 authorization header.
   219  
   220  	// Save the original auth.
   221  	origAuth := req.Header.Get("Authorization")
   222  	// Strip out accessKeyID from:
   223  	// Credential=<access-key-id>/<date>/<aws-region>/<aws-service>/aws4_request
   224  	regCred := regexp.MustCompile("Credential=([A-Z0-9]+)/")
   225  	newAuth := regCred.ReplaceAllString(origAuth, "Credential=**REDACTED**/")
   226  
   227  	// Strip out 256-bit signature from: Signature=<256-bit signature>
   228  	regSign := regexp.MustCompile("Signature=([[0-9a-f]+)")
   229  	newAuth = regSign.ReplaceAllString(newAuth, "Signature=**REDACTED**")
   230  
   231  	// Set a temporary redacted auth
   232  	req.Header.Set("Authorization", newAuth)
   233  }
   234  
   235  // dumpHTTP - dump HTTP request and response.
   236  func (adm AdminClient) dumpHTTP(req *http.Request, resp *http.Response) error {
   237  	// Starts http dump.
   238  	_, err := fmt.Fprintln(adm.traceOutput, "---------START-HTTP---------")
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	// Filter out Signature field from Authorization header.
   244  	adm.filterSignature(req)
   245  
   246  	// Only display request header.
   247  	reqTrace, err := httputil.DumpRequestOut(req, false)
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	// Write request to trace output.
   253  	_, err = fmt.Fprint(adm.traceOutput, string(reqTrace))
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	// Only display response header.
   259  	var respTrace []byte
   260  
   261  	// For errors we make sure to dump response body as well.
   262  	if resp.StatusCode != http.StatusOK &&
   263  		resp.StatusCode != http.StatusPartialContent &&
   264  		resp.StatusCode != http.StatusNoContent {
   265  		respTrace, err = httputil.DumpResponse(resp, true)
   266  		if err != nil {
   267  			return err
   268  		}
   269  	} else {
   270  		// WORKAROUND for https://github.com/golang/go/issues/13942.
   271  		// httputil.DumpResponse does not print response headers for
   272  		// all successful calls which have response ContentLength set
   273  		// to zero. Keep this workaround until the above bug is fixed.
   274  		if resp.ContentLength == 0 {
   275  			var buffer bytes.Buffer
   276  			if err = resp.Header.Write(&buffer); err != nil {
   277  				return err
   278  			}
   279  			respTrace = buffer.Bytes()
   280  			respTrace = append(respTrace, []byte("\r\n")...)
   281  		} else {
   282  			respTrace, err = httputil.DumpResponse(resp, false)
   283  			if err != nil {
   284  				return err
   285  			}
   286  		}
   287  	}
   288  	// Write response to trace output.
   289  	_, err = fmt.Fprint(adm.traceOutput, strings.TrimSuffix(string(respTrace), "\r\n"))
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	// Ends the http dump.
   295  	_, err = fmt.Fprintln(adm.traceOutput, "---------END-HTTP---------")
   296  	return err
   297  }
   298  
   299  // do - execute http request.
   300  func (adm AdminClient) do(req *http.Request) (*http.Response, error) {
   301  	resp, err := adm.httpClient.Do(req)
   302  	if err != nil {
   303  		// Handle this specifically for now until future Golang versions fix this issue properly.
   304  		if urlErr, ok := err.(*url.Error); ok {
   305  			if strings.Contains(urlErr.Err.Error(), "EOF") {
   306  				return nil, &url.Error{
   307  					Op:  urlErr.Op,
   308  					URL: urlErr.URL,
   309  					Err: errors.New("Connection closed by foreign host " + urlErr.URL + ". Retry again."),
   310  				}
   311  			}
   312  		}
   313  		return nil, err
   314  	}
   315  
   316  	// Response cannot be non-nil, report if its the case.
   317  	if resp == nil {
   318  		msg := "Response is empty. " // + reportIssue
   319  		return nil, ErrInvalidArgument(msg)
   320  	}
   321  
   322  	// If trace is enabled, dump http request and response.
   323  	if adm.isTraceEnabled {
   324  		err = adm.dumpHTTP(req, resp)
   325  		if err != nil {
   326  			return nil, err
   327  		}
   328  	}
   329  	return resp, nil
   330  }
   331  
   332  // List of success status.
   333  var successStatus = []int{
   334  	http.StatusOK,
   335  	http.StatusNoContent,
   336  	http.StatusPartialContent,
   337  }
   338  
   339  // executeMethod - instantiates a given method, and retries the
   340  // request upon any error up to maxRetries attempts in a binomially
   341  // delayed manner using a standard back off algorithm.
   342  func (adm AdminClient) executeMethod(ctx context.Context, method string, reqData requestData) (res *http.Response, err error) {
   343  	var reqRetry = MaxRetry // Indicates how many times we can retry the request
   344  	defer func() {
   345  		if err != nil {
   346  			// close idle connections before returning, upon error.
   347  			adm.httpClient.CloseIdleConnections()
   348  		}
   349  	}()
   350  
   351  	// Create cancel context to control 'newRetryTimer' go routine.
   352  	retryCtx, cancel := context.WithCancel(ctx)
   353  
   354  	// Indicate to our routine to exit cleanly upon return.
   355  	defer cancel()
   356  
   357  	for range adm.newRetryTimer(retryCtx, reqRetry, DefaultRetryUnit, DefaultRetryCap, MaxJitter) {
   358  		// Instantiate a new request.
   359  		var req *http.Request
   360  		req, err = adm.newRequest(ctx, method, reqData)
   361  		if err != nil {
   362  			return nil, err
   363  		}
   364  
   365  		// Initiate the request.
   366  		res, err = adm.do(req)
   367  		if err != nil {
   368  			// Give up right away if it is a connection refused problem
   369  			if errors.Is(err, syscall.ECONNREFUSED) {
   370  				return nil, err
   371  			}
   372  			if err == context.Canceled || err == context.DeadlineExceeded {
   373  				return nil, err
   374  			}
   375  			// retry all network errors.
   376  			continue
   377  		}
   378  
   379  		// For any known successful http status, return quickly.
   380  		for _, httpStatus := range successStatus {
   381  			if httpStatus == res.StatusCode {
   382  				return res, nil
   383  			}
   384  		}
   385  
   386  		// Read the body to be saved later.
   387  		errBodyBytes, err := ioutil.ReadAll(res.Body)
   388  		// res.Body should be closed
   389  		closeResponse(res)
   390  		if err != nil {
   391  			return nil, err
   392  		}
   393  
   394  		// Save the body.
   395  		errBodySeeker := bytes.NewReader(errBodyBytes)
   396  		res.Body = ioutil.NopCloser(errBodySeeker)
   397  
   398  		// For errors verify if its retryable otherwise fail quickly.
   399  		errResponse := ToErrorResponse(httpRespToErrorResponse(res))
   400  
   401  		// Save the body back again.
   402  		errBodySeeker.Seek(0, 0) // Seek back to starting point.
   403  		res.Body = ioutil.NopCloser(errBodySeeker)
   404  
   405  		// Verify if error response code is retryable.
   406  		if isS3CodeRetryable(errResponse.Code) {
   407  			continue // Retry.
   408  		}
   409  
   410  		// Verify if http status code is retryable.
   411  		if isHTTPStatusRetryable(res.StatusCode) {
   412  			continue // Retry.
   413  		}
   414  
   415  		break
   416  	}
   417  
   418  	// Return an error when retry is canceled or deadlined
   419  	if e := retryCtx.Err(); e != nil {
   420  		return nil, e
   421  	}
   422  
   423  	return res, err
   424  }
   425  
   426  // set User agent.
   427  func (adm AdminClient) setUserAgent(req *http.Request) {
   428  	req.Header.Set("User-Agent", libraryUserAgent)
   429  	if adm.appInfo.appName != "" && adm.appInfo.appVersion != "" {
   430  		req.Header.Set("User-Agent", libraryUserAgent+" "+adm.appInfo.appName+"/"+adm.appInfo.appVersion)
   431  	}
   432  }
   433  
   434  func (adm AdminClient) getSecretKey() string {
   435  	value, err := adm.credsProvider.Get()
   436  	if err != nil {
   437  		// Return empty, call will fail.
   438  		return ""
   439  	}
   440  
   441  	return value.SecretAccessKey
   442  }
   443  
   444  // newRequest - instantiate a new HTTP request for a given method.
   445  func (adm AdminClient) newRequest(ctx context.Context, method string, reqData requestData) (req *http.Request, err error) {
   446  	// If no method is supplied default to 'POST'.
   447  	if method == "" {
   448  		method = "POST"
   449  	}
   450  
   451  	// Default all requests to ""
   452  	location := ""
   453  
   454  	// Construct a new target URL.
   455  	targetURL, err := adm.makeTargetURL(reqData)
   456  	if err != nil {
   457  		return nil, err
   458  	}
   459  
   460  	// Initialize a new HTTP request for the method.
   461  	req, err = http.NewRequestWithContext(ctx, method, targetURL.String(), nil)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  
   466  	value, err := adm.credsProvider.Get()
   467  	if err != nil {
   468  		return nil, err
   469  	}
   470  
   471  	var (
   472  		accessKeyID     = value.AccessKeyID
   473  		secretAccessKey = value.SecretAccessKey
   474  		sessionToken    = value.SessionToken
   475  	)
   476  
   477  	adm.setUserAgent(req)
   478  	for k, v := range reqData.customHeaders {
   479  		req.Header.Set(k, v[0])
   480  	}
   481  	if length := len(reqData.content); length > 0 {
   482  		req.ContentLength = int64(length)
   483  	}
   484  	sum := sha256.Sum256(reqData.content)
   485  	req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:]))
   486  	req.Body = ioutil.NopCloser(bytes.NewReader(reqData.content))
   487  
   488  	req = signer.SignV4(*req, accessKeyID, secretAccessKey, sessionToken, location)
   489  	return req, nil
   490  }
   491  
   492  // makeTargetURL make a new target url.
   493  func (adm AdminClient) makeTargetURL(r requestData) (*url.URL, error) {
   494  
   495  	host := adm.endpointURL.Host
   496  	scheme := adm.endpointURL.Scheme
   497  
   498  	urlStr := scheme + "://" + host + libraryAdminURLPrefix + r.relPath
   499  
   500  	// If there are any query values, add them to the end.
   501  	if len(r.queryValues) > 0 {
   502  		urlStr = urlStr + "?" + s3utils.QueryEncode(r.queryValues)
   503  	}
   504  	u, err := url.Parse(urlStr)
   505  	if err != nil {
   506  		return nil, err
   507  	}
   508  	return u, nil
   509  }