github.com/minio/madmin-go/v2@v2.2.1/anonymous-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  	"net/http"
    32  	"net/http/cookiejar"
    33  	"net/http/httptrace"
    34  	"net/http/httputil"
    35  	"net/url"
    36  	"os"
    37  	"strings"
    38  
    39  	"github.com/minio/minio-go/v7/pkg/s3utils"
    40  	"golang.org/x/net/publicsuffix"
    41  )
    42  
    43  // AnonymousClient implements an anonymous http client for MinIO
    44  type AnonymousClient struct {
    45  	// Parsed endpoint url provided by the caller
    46  	endpointURL *url.URL
    47  	// Indicate whether we are using https or not
    48  	secure bool
    49  	// Needs allocation.
    50  	httpClient *http.Client
    51  	// Advanced functionality.
    52  	isTraceEnabled bool
    53  	traceOutput    io.Writer
    54  }
    55  
    56  func NewAnonymousClientNoEndpoint() (*AnonymousClient, error) {
    57  	// Initialize cookies to preserve server sent cookies if any and replay
    58  	// them upon each request.
    59  	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	clnt := new(AnonymousClient)
    65  
    66  	// Instantiate http client and bucket location cache.
    67  	clnt.httpClient = &http.Client{
    68  		Jar:       jar,
    69  		Transport: DefaultTransport(true),
    70  	}
    71  
    72  	return clnt, nil
    73  }
    74  
    75  // NewAnonymousClient can be used for anonymous APIs without credentials set
    76  func NewAnonymousClient(endpoint string, secure bool) (*AnonymousClient, error) {
    77  	// Initialize cookies to preserve server sent cookies if any and replay
    78  	// them upon each request.
    79  	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	// construct endpoint.
    85  	endpointURL, err := getEndpointURL(endpoint, secure)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	clnt := new(AnonymousClient)
    91  
    92  	// Remember whether we are using https or not
    93  	clnt.secure = secure
    94  
    95  	// Save endpoint URL, user agent for future uses.
    96  	clnt.endpointURL = endpointURL
    97  
    98  	// Instantiate http client and bucket location cache.
    99  	clnt.httpClient = &http.Client{
   100  		Jar:       jar,
   101  		Transport: DefaultTransport(secure),
   102  	}
   103  
   104  	return clnt, nil
   105  }
   106  
   107  // SetCustomTransport - set new custom transport.
   108  func (an *AnonymousClient) SetCustomTransport(customHTTPTransport http.RoundTripper) {
   109  	// Set this to override default transport
   110  	// ``http.DefaultTransport``.
   111  	//
   112  	// This transport is usually needed for debugging OR to add your
   113  	// own custom TLS certificates on the client transport, for custom
   114  	// CA's and certs which are not part of standard certificate
   115  	// authority follow this example :-
   116  	//
   117  	//   tr := &http.Transport{
   118  	//           TLSClientConfig:    &tls.Config{RootCAs: pool},
   119  	//           DisableCompression: true,
   120  	//   }
   121  	//   api.SetTransport(tr)
   122  	//
   123  	if an.httpClient != nil {
   124  		an.httpClient.Transport = customHTTPTransport
   125  	}
   126  }
   127  
   128  // TraceOn - enable HTTP tracing.
   129  func (an *AnonymousClient) TraceOn(outputStream io.Writer) {
   130  	// if outputStream is nil then default to os.Stdout.
   131  	if outputStream == nil {
   132  		outputStream = os.Stdout
   133  	}
   134  	// Sets a new output stream.
   135  	an.traceOutput = outputStream
   136  
   137  	// Enable tracing.
   138  	an.isTraceEnabled = true
   139  }
   140  
   141  // executeMethod - does a simple http request to the target with parameters provided in the request
   142  func (an AnonymousClient) executeMethod(ctx context.Context, method string, reqData requestData, trace *httptrace.ClientTrace) (res *http.Response, err error) {
   143  	defer func() {
   144  		if err != nil {
   145  			// close idle connections before returning, upon error.
   146  			an.httpClient.CloseIdleConnections()
   147  		}
   148  	}()
   149  
   150  	// Instantiate a new request.
   151  	var req *http.Request
   152  	req, err = an.newRequest(ctx, method, reqData)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	if trace != nil {
   158  		req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
   159  	}
   160  
   161  	// Initiate the request.
   162  	res, err = an.do(req)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	return res, err
   168  }
   169  
   170  // newRequest - instantiate a new HTTP request for a given method.
   171  func (an AnonymousClient) newRequest(ctx context.Context, method string, reqData requestData) (req *http.Request, err error) {
   172  	// If no method is supplied default to 'POST'.
   173  	if method == "" {
   174  		method = "POST"
   175  	}
   176  
   177  	// Construct a new target URL.
   178  	targetURL, err := an.makeTargetURL(reqData)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	// Initialize a new HTTP request for the method.
   184  	req, err = http.NewRequestWithContext(ctx, method, targetURL.String(), nil)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	for k, v := range reqData.customHeaders {
   189  		req.Header.Set(k, v[0])
   190  	}
   191  	if length := len(reqData.content); length > 0 {
   192  		req.ContentLength = int64(length)
   193  	}
   194  	sum := sha256.Sum256(reqData.content)
   195  	req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:]))
   196  	req.Body = ioutil.NopCloser(bytes.NewReader(reqData.content))
   197  
   198  	return req, nil
   199  }
   200  
   201  // makeTargetURL make a new target url.
   202  func (an AnonymousClient) makeTargetURL(r requestData) (*url.URL, error) {
   203  	u := an.endpointURL
   204  	if r.endpointOverride != nil {
   205  		u = r.endpointOverride
   206  	} else if u == nil {
   207  		return nil, errors.New("endpoint not configured unable to use AnonymousClient")
   208  	}
   209  	host := u.Host
   210  	scheme := u.Scheme
   211  
   212  	urlStr := scheme + "://" + host + r.relPath
   213  
   214  	// If there are any query values, add them to the end.
   215  	if len(r.queryValues) > 0 {
   216  		urlStr = urlStr + "?" + s3utils.QueryEncode(r.queryValues)
   217  	}
   218  	u, err := url.Parse(urlStr)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  	return u, nil
   223  }
   224  
   225  // do - execute http request.
   226  func (an AnonymousClient) do(req *http.Request) (*http.Response, error) {
   227  	resp, err := an.httpClient.Do(req)
   228  	if err != nil {
   229  		// Handle this specifically for now until future Golang versions fix this issue properly.
   230  		if urlErr, ok := err.(*url.Error); ok {
   231  			if strings.Contains(urlErr.Err.Error(), "EOF") {
   232  				return nil, &url.Error{
   233  					Op:  urlErr.Op,
   234  					URL: urlErr.URL,
   235  					Err: errors.New("Connection closed by foreign host " + urlErr.URL + ". Retry again."),
   236  				}
   237  			}
   238  		}
   239  		return nil, err
   240  	}
   241  
   242  	// Response cannot be non-nil, report if its the case.
   243  	if resp == nil {
   244  		msg := "Response is empty. " // + reportIssue
   245  		return nil, ErrInvalidArgument(msg)
   246  	}
   247  
   248  	// If trace is enabled, dump http request and response.
   249  	if an.isTraceEnabled {
   250  		err = an.dumpHTTP(req, resp)
   251  		if err != nil {
   252  			return nil, err
   253  		}
   254  	}
   255  
   256  	return resp, nil
   257  }
   258  
   259  // dumpHTTP - dump HTTP request and response.
   260  func (an AnonymousClient) dumpHTTP(req *http.Request, resp *http.Response) error {
   261  	// Starts http dump.
   262  	_, err := fmt.Fprintln(an.traceOutput, "---------START-HTTP---------")
   263  	if err != nil {
   264  		return err
   265  	}
   266  
   267  	// Only display request header.
   268  	reqTrace, err := httputil.DumpRequestOut(req, false)
   269  	if err != nil {
   270  		return err
   271  	}
   272  
   273  	// Write request to trace output.
   274  	_, err = fmt.Fprint(an.traceOutput, string(reqTrace))
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	// Only display response header.
   280  	var respTrace []byte
   281  
   282  	// For errors we make sure to dump response body as well.
   283  	if resp.StatusCode != http.StatusOK &&
   284  		resp.StatusCode != http.StatusPartialContent &&
   285  		resp.StatusCode != http.StatusNoContent {
   286  		respTrace, err = httputil.DumpResponse(resp, true)
   287  		if err != nil {
   288  			return err
   289  		}
   290  	} else {
   291  		// WORKAROUND for https://github.com/golang/go/issues/13942.
   292  		// httputil.DumpResponse does not print response headers for
   293  		// all successful calls which have response ContentLength set
   294  		// to zero. Keep this workaround until the above bug is fixed.
   295  		if resp.ContentLength == 0 {
   296  			var buffer bytes.Buffer
   297  			if err = resp.Header.Write(&buffer); err != nil {
   298  				return err
   299  			}
   300  			respTrace = buffer.Bytes()
   301  			respTrace = append(respTrace, []byte("\r\n")...)
   302  		} else {
   303  			respTrace, err = httputil.DumpResponse(resp, false)
   304  			if err != nil {
   305  				return err
   306  			}
   307  		}
   308  	}
   309  	// Write response to trace output.
   310  	_, err = fmt.Fprint(an.traceOutput, strings.TrimSuffix(string(respTrace), "\r\n"))
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	// Ends the http dump.
   316  	_, err = fmt.Fprintln(an.traceOutput, "---------END-HTTP---------")
   317  	return err
   318  }