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

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