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