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