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 }