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