github.com/minio/madmin-go@v1.7.5/api.go (about)

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