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 }