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