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 }