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