github.com/minio/madmin-go/v3@v3.0.51/api.go (about)

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