storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/madmin/api.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2016-2020 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 "math/rand" 29 "net/http" 30 "net/http/cookiejar" 31 "net/http/httputil" 32 "net/url" 33 "os" 34 "regexp" 35 "runtime" 36 "strings" 37 "syscall" 38 "time" 39 40 "github.com/minio/minio-go/v7/pkg/credentials" 41 "github.com/minio/minio-go/v7/pkg/s3utils" 42 "github.com/minio/minio-go/v7/pkg/signer" 43 "golang.org/x/net/publicsuffix" 44 ) 45 46 // AdminClient implements Amazon S3 compatible methods. 47 type AdminClient struct { 48 /// Standard options. 49 50 // Parsed endpoint url provided by the user. 51 endpointURL *url.URL 52 53 // Holds various credential providers. 54 credsProvider *credentials.Credentials 55 56 // User supplied. 57 appInfo struct { 58 appName string 59 appVersion string 60 } 61 62 // Indicate whether we are using https or not 63 secure bool 64 65 // Needs allocation. 66 httpClient *http.Client 67 68 random *rand.Rand 69 70 // Advanced functionality. 71 isTraceEnabled bool 72 traceOutput io.Writer 73 } 74 75 // Global constants. 76 const ( 77 libraryName = "madmin-go" 78 libraryVersion = "0.0.1" 79 80 libraryAdminURLPrefix = "/minio/admin" 81 ) 82 83 // User Agent should always following the below style. 84 // Please open an issue to discuss any new changes here. 85 // 86 // MinIO (OS; ARCH) LIB/VER APP/VER 87 const ( 88 libraryUserAgentPrefix = "MinIO (" + runtime.GOOS + "; " + runtime.GOARCH + ") " 89 libraryUserAgent = libraryUserAgentPrefix + libraryName + "/" + libraryVersion 90 ) 91 92 // Options for New method 93 type Options struct { 94 Creds *credentials.Credentials 95 Secure bool 96 // Add future fields here 97 } 98 99 // New - instantiate minio admin client 100 func New(endpoint string, accessKeyID, secretAccessKey string, secure bool) (*AdminClient, error) { 101 creds := credentials.NewStaticV4(accessKeyID, secretAccessKey, "") 102 103 clnt, err := privateNew(endpoint, creds, secure) 104 if err != nil { 105 return nil, err 106 } 107 return clnt, nil 108 } 109 110 // NewWithOptions - instantiate minio admin client with options. 111 func NewWithOptions(endpoint string, opts *Options) (*AdminClient, error) { 112 clnt, err := privateNew(endpoint, opts.Creds, opts.Secure) 113 if err != nil { 114 return nil, err 115 } 116 return clnt, nil 117 } 118 119 func privateNew(endpoint string, creds *credentials.Credentials, secure bool) (*AdminClient, error) { 120 // Initialize cookies to preserve server sent cookies if any and replay 121 // them upon each request. 122 jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) 123 if err != nil { 124 return nil, err 125 } 126 127 // construct endpoint. 128 endpointURL, err := getEndpointURL(endpoint, secure) 129 if err != nil { 130 return nil, err 131 } 132 133 clnt := new(AdminClient) 134 135 // Save the credentials. 136 clnt.credsProvider = creds 137 138 // Remember whether we are using https or not 139 clnt.secure = secure 140 141 // Save endpoint URL, user agent for future uses. 142 clnt.endpointURL = endpointURL 143 144 // Instantiate http client and bucket location cache. 145 clnt.httpClient = &http.Client{ 146 Jar: jar, 147 Transport: DefaultTransport(secure), 148 } 149 150 // Add locked pseudo-random number generator. 151 clnt.random = rand.New(&lockedRandSource{src: rand.NewSource(time.Now().UTC().UnixNano())}) 152 153 // Return. 154 return clnt, nil 155 } 156 157 // SetAppInfo - add application details to user agent. 158 func (adm *AdminClient) SetAppInfo(appName string, appVersion string) { 159 // if app name and version is not set, we do not a new user 160 // agent. 161 if appName != "" && appVersion != "" { 162 adm.appInfo.appName = appName 163 adm.appInfo.appVersion = appVersion 164 } 165 } 166 167 // SetCustomTransport - set new custom transport. 168 func (adm *AdminClient) SetCustomTransport(customHTTPTransport http.RoundTripper) { 169 // Set this to override default transport 170 // ``http.DefaultTransport``. 171 // 172 // This transport is usually needed for debugging OR to add your 173 // own custom TLS certificates on the client transport, for custom 174 // CA's and certs which are not part of standard certificate 175 // authority follow this example :- 176 // 177 // tr := &http.Transport{ 178 // TLSClientConfig: &tls.Config{RootCAs: pool}, 179 // DisableCompression: true, 180 // } 181 // api.SetTransport(tr) 182 // 183 if adm.httpClient != nil { 184 adm.httpClient.Transport = customHTTPTransport 185 } 186 } 187 188 // TraceOn - enable HTTP tracing. 189 func (adm *AdminClient) TraceOn(outputStream io.Writer) { 190 // if outputStream is nil then default to os.Stdout. 191 if outputStream == nil { 192 outputStream = os.Stdout 193 } 194 // Sets a new output stream. 195 adm.traceOutput = outputStream 196 197 // Enable tracing. 198 adm.isTraceEnabled = true 199 } 200 201 // TraceOff - disable HTTP tracing. 202 func (adm *AdminClient) TraceOff() { 203 // Disable tracing. 204 adm.isTraceEnabled = false 205 } 206 207 // requestMetadata - is container for all the values to make a 208 // request. 209 type requestData struct { 210 customHeaders http.Header 211 queryValues url.Values 212 relPath string // URL path relative to admin API base endpoint 213 content []byte 214 } 215 216 // Filter out signature value from Authorization header. 217 func (adm AdminClient) filterSignature(req *http.Request) { 218 /// Signature V4 authorization header. 219 220 // Save the original auth. 221 origAuth := req.Header.Get("Authorization") 222 // Strip out accessKeyID from: 223 // Credential=<access-key-id>/<date>/<aws-region>/<aws-service>/aws4_request 224 regCred := regexp.MustCompile("Credential=([A-Z0-9]+)/") 225 newAuth := regCred.ReplaceAllString(origAuth, "Credential=**REDACTED**/") 226 227 // Strip out 256-bit signature from: Signature=<256-bit signature> 228 regSign := regexp.MustCompile("Signature=([[0-9a-f]+)") 229 newAuth = regSign.ReplaceAllString(newAuth, "Signature=**REDACTED**") 230 231 // Set a temporary redacted auth 232 req.Header.Set("Authorization", newAuth) 233 } 234 235 // dumpHTTP - dump HTTP request and response. 236 func (adm AdminClient) dumpHTTP(req *http.Request, resp *http.Response) error { 237 // Starts http dump. 238 _, err := fmt.Fprintln(adm.traceOutput, "---------START-HTTP---------") 239 if err != nil { 240 return err 241 } 242 243 // Filter out Signature field from Authorization header. 244 adm.filterSignature(req) 245 246 // Only display request header. 247 reqTrace, err := httputil.DumpRequestOut(req, false) 248 if err != nil { 249 return err 250 } 251 252 // Write request to trace output. 253 _, err = fmt.Fprint(adm.traceOutput, string(reqTrace)) 254 if err != nil { 255 return err 256 } 257 258 // Only display response header. 259 var respTrace []byte 260 261 // For errors we make sure to dump response body as well. 262 if resp.StatusCode != http.StatusOK && 263 resp.StatusCode != http.StatusPartialContent && 264 resp.StatusCode != http.StatusNoContent { 265 respTrace, err = httputil.DumpResponse(resp, true) 266 if err != nil { 267 return err 268 } 269 } else { 270 // WORKAROUND for https://github.com/golang/go/issues/13942. 271 // httputil.DumpResponse does not print response headers for 272 // all successful calls which have response ContentLength set 273 // to zero. Keep this workaround until the above bug is fixed. 274 if resp.ContentLength == 0 { 275 var buffer bytes.Buffer 276 if err = resp.Header.Write(&buffer); err != nil { 277 return err 278 } 279 respTrace = buffer.Bytes() 280 respTrace = append(respTrace, []byte("\r\n")...) 281 } else { 282 respTrace, err = httputil.DumpResponse(resp, false) 283 if err != nil { 284 return err 285 } 286 } 287 } 288 // Write response to trace output. 289 _, err = fmt.Fprint(adm.traceOutput, strings.TrimSuffix(string(respTrace), "\r\n")) 290 if err != nil { 291 return err 292 } 293 294 // Ends the http dump. 295 _, err = fmt.Fprintln(adm.traceOutput, "---------END-HTTP---------") 296 return err 297 } 298 299 // do - execute http request. 300 func (adm AdminClient) do(req *http.Request) (*http.Response, error) { 301 resp, err := adm.httpClient.Do(req) 302 if err != nil { 303 // Handle this specifically for now until future Golang versions fix this issue properly. 304 if urlErr, ok := err.(*url.Error); ok { 305 if strings.Contains(urlErr.Err.Error(), "EOF") { 306 return nil, &url.Error{ 307 Op: urlErr.Op, 308 URL: urlErr.URL, 309 Err: errors.New("Connection closed by foreign host " + urlErr.URL + ". Retry again."), 310 } 311 } 312 } 313 return nil, err 314 } 315 316 // Response cannot be non-nil, report if its the case. 317 if resp == nil { 318 msg := "Response is empty. " // + reportIssue 319 return nil, ErrInvalidArgument(msg) 320 } 321 322 // If trace is enabled, dump http request and response. 323 if adm.isTraceEnabled { 324 err = adm.dumpHTTP(req, resp) 325 if err != nil { 326 return nil, err 327 } 328 } 329 return resp, nil 330 } 331 332 // List of success status. 333 var successStatus = []int{ 334 http.StatusOK, 335 http.StatusNoContent, 336 http.StatusPartialContent, 337 } 338 339 // executeMethod - instantiates a given method, and retries the 340 // request upon any error up to maxRetries attempts in a binomially 341 // delayed manner using a standard back off algorithm. 342 func (adm AdminClient) executeMethod(ctx context.Context, method string, reqData requestData) (res *http.Response, err error) { 343 var reqRetry = MaxRetry // Indicates how many times we can retry the request 344 defer func() { 345 if err != nil { 346 // close idle connections before returning, upon error. 347 adm.httpClient.CloseIdleConnections() 348 } 349 }() 350 351 // Create cancel context to control 'newRetryTimer' go routine. 352 retryCtx, cancel := context.WithCancel(ctx) 353 354 // Indicate to our routine to exit cleanly upon return. 355 defer cancel() 356 357 for range adm.newRetryTimer(retryCtx, reqRetry, DefaultRetryUnit, DefaultRetryCap, MaxJitter) { 358 // Instantiate a new request. 359 var req *http.Request 360 req, err = adm.newRequest(ctx, method, reqData) 361 if err != nil { 362 return nil, err 363 } 364 365 // Initiate the request. 366 res, err = adm.do(req) 367 if err != nil { 368 // Give up right away if it is a connection refused problem 369 if errors.Is(err, syscall.ECONNREFUSED) { 370 return nil, err 371 } 372 if err == context.Canceled || err == context.DeadlineExceeded { 373 return nil, err 374 } 375 // retry all network errors. 376 continue 377 } 378 379 // For any known successful http status, return quickly. 380 for _, httpStatus := range successStatus { 381 if httpStatus == res.StatusCode { 382 return res, nil 383 } 384 } 385 386 // Read the body to be saved later. 387 errBodyBytes, err := ioutil.ReadAll(res.Body) 388 // res.Body should be closed 389 closeResponse(res) 390 if err != nil { 391 return nil, err 392 } 393 394 // Save the body. 395 errBodySeeker := bytes.NewReader(errBodyBytes) 396 res.Body = ioutil.NopCloser(errBodySeeker) 397 398 // For errors verify if its retryable otherwise fail quickly. 399 errResponse := ToErrorResponse(httpRespToErrorResponse(res)) 400 401 // Save the body back again. 402 errBodySeeker.Seek(0, 0) // Seek back to starting point. 403 res.Body = ioutil.NopCloser(errBodySeeker) 404 405 // Verify if error response code is retryable. 406 if isS3CodeRetryable(errResponse.Code) { 407 continue // Retry. 408 } 409 410 // Verify if http status code is retryable. 411 if isHTTPStatusRetryable(res.StatusCode) { 412 continue // Retry. 413 } 414 415 break 416 } 417 418 // Return an error when retry is canceled or deadlined 419 if e := retryCtx.Err(); e != nil { 420 return nil, e 421 } 422 423 return res, err 424 } 425 426 // set User agent. 427 func (adm AdminClient) setUserAgent(req *http.Request) { 428 req.Header.Set("User-Agent", libraryUserAgent) 429 if adm.appInfo.appName != "" && adm.appInfo.appVersion != "" { 430 req.Header.Set("User-Agent", libraryUserAgent+" "+adm.appInfo.appName+"/"+adm.appInfo.appVersion) 431 } 432 } 433 434 func (adm AdminClient) getSecretKey() string { 435 value, err := adm.credsProvider.Get() 436 if err != nil { 437 // Return empty, call will fail. 438 return "" 439 } 440 441 return value.SecretAccessKey 442 } 443 444 // newRequest - instantiate a new HTTP request for a given method. 445 func (adm AdminClient) newRequest(ctx context.Context, method string, reqData requestData) (req *http.Request, err error) { 446 // If no method is supplied default to 'POST'. 447 if method == "" { 448 method = "POST" 449 } 450 451 // Default all requests to "" 452 location := "" 453 454 // Construct a new target URL. 455 targetURL, err := adm.makeTargetURL(reqData) 456 if err != nil { 457 return nil, err 458 } 459 460 // Initialize a new HTTP request for the method. 461 req, err = http.NewRequestWithContext(ctx, method, targetURL.String(), nil) 462 if err != nil { 463 return nil, err 464 } 465 466 value, err := adm.credsProvider.Get() 467 if err != nil { 468 return nil, err 469 } 470 471 var ( 472 accessKeyID = value.AccessKeyID 473 secretAccessKey = value.SecretAccessKey 474 sessionToken = value.SessionToken 475 ) 476 477 adm.setUserAgent(req) 478 for k, v := range reqData.customHeaders { 479 req.Header.Set(k, v[0]) 480 } 481 if length := len(reqData.content); length > 0 { 482 req.ContentLength = int64(length) 483 } 484 sum := sha256.Sum256(reqData.content) 485 req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:])) 486 req.Body = ioutil.NopCloser(bytes.NewReader(reqData.content)) 487 488 req = signer.SignV4(*req, accessKeyID, secretAccessKey, sessionToken, location) 489 return req, nil 490 } 491 492 // makeTargetURL make a new target url. 493 func (adm AdminClient) makeTargetURL(r requestData) (*url.URL, error) { 494 495 host := adm.endpointURL.Host 496 scheme := adm.endpointURL.Scheme 497 498 urlStr := scheme + "://" + host + libraryAdminURLPrefix + r.relPath 499 500 // If there are any query values, add them to the end. 501 if len(r.queryValues) > 0 { 502 urlStr = urlStr + "?" + s3utils.QueryEncode(r.queryValues) 503 } 504 u, err := url.Parse(urlStr) 505 if err != nil { 506 return nil, err 507 } 508 return u, nil 509 }