using System.Diagnostics.CodeAnalysis; using System.Net; using Minio.Credentials; using Minio.DataModel; using Minio.DataModel.Args; using Minio.DataModel.Result; using Minio.Exceptions; using Minio.Handlers; using Minio.Helper; namespace Minio; public static class RequestExtensions { [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "This is done in the interface. String is provided here for convenience")] public static Task WrapperGetAsync(this IMinioClient minioClient, string url) { if (minioClient is null) throw new ArgumentNullException(nameof(minioClient)); return minioClient.WrapperGetAsync(new Uri(url)); } /// /// Runs httpClient's PutObjectAsync method /// [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "This is done in the interface. String is provided here for convenience")] public static Task WrapperPutAsync(this IMinioClient minioClient, string url, StreamContent strm) { if (minioClient is null) throw new ArgumentNullException(nameof(minioClient)); return minioClient.WrapperPutAsync(new Uri(url), strm); } /// /// Actual doer that executes the request on the server /// /// /// List of handlers to override default handling /// The build of HttpRequestMessageBuilder /// boolean; if true role credentials, otherwise IAM user /// Optional cancellation token to cancel the operation /// ResponseResult internal static Task ExecuteTaskAsync(this IMinioClient minioClient, IEnumerable errorHandlers, HttpRequestMessageBuilder requestMessageBuilder, bool isSts = false, CancellationToken cancellationToken = default) { Task responseResult; try { if (minioClient.Config.RequestTimeout > 0) { using var internalTokenSource = new CancellationTokenSource(new TimeSpan(0, 0, 0, 0, minioClient.Config.RequestTimeout)); using var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(internalTokenSource.Token, cancellationToken); cancellationToken = timeoutTokenSource.Token; } responseResult = minioClient.ExecuteWithRetry( async () => await minioClient.ExecuteTaskCoreAsync(errorHandlers, requestMessageBuilder, isSts, cancellationToken).ConfigureAwait(false)); } catch (Exception ex) { Console.WriteLine($"\n\n *** ExecuteTaskAsync::Threw an exception => {ex.Message}"); throw; } return responseResult; } private static async Task ExecuteTaskCoreAsync(this IMinioClient minioClient, IEnumerable errorHandlers, HttpRequestMessageBuilder requestMessageBuilder, bool isSts = false, CancellationToken cancellationToken = default) { var startTime = DateTime.Now; var v4Authenticator = new V4Authenticator(minioClient.Config.Secure, minioClient.Config.AccessKey, minioClient.Config.SecretKey, minioClient.Config.Region, minioClient.Config.SessionToken); requestMessageBuilder.AddOrUpdateHeaderParameter("Authorization", v4Authenticator.Authenticate(requestMessageBuilder, isSts)); var request = requestMessageBuilder.Request; ResponseResult responseResult = null; try { var response = await minioClient.Config.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); responseResult = new ResponseResult(request, response); if (requestMessageBuilder.ResponseWriter is not null) requestMessageBuilder.ResponseWriter(responseResult.ContentStream); if (requestMessageBuilder.FunctionResponseWriter is not null) await requestMessageBuilder.FunctionResponseWriter(responseResult.ContentStream, cancellationToken) .ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception e) { responseResult?.Dispose(); responseResult = new ResponseResult(request, e); } if (responseResult.StatusCode == HttpStatusCode.NotFound) { if (request.Method == HttpMethod.Head) { Exception ex = new BucketNotFoundException(); responseResult.Exception = ex; return responseResult; } if (request.RequestUri.ToString().Contains("lock", StringComparison.OrdinalIgnoreCase) && request.Method == HttpMethod.Get) { Exception ex = new MissingObjectLockConfigurationException(); responseResult.Exception = ex; return responseResult; } } minioClient.HandleIfErrorResponse(responseResult, errorHandlers, startTime); return responseResult; } private static Task ExecuteWithRetry(this IMinioClient minioClient, Func> executeRequestCallback) { return minioClient.Config.RetryPolicyHandler is null ? executeRequestCallback() : minioClient.Config.RetryPolicyHandler.Handle(executeRequestCallback); } /// /// Constructs a HttpRequestMessageBuilder using bucket/object names from Args. /// Calls overloaded CreateRequest method. /// /// /// The direct descendant of BucketArgs class, args with populated values from Input /// A HttpRequestMessageBuilder internal static async Task CreateRequest(this IMinioClient minioClient, BucketArgs args) where T : BucketArgs { ArgsCheck(args); var requestMessageBuilder = await minioClient.CreateRequest(args.RequestMethod, args.BucketName, headerMap: args.Headers, isBucketCreationRequest: args.IsBucketCreationRequest).ConfigureAwait(false); return args.BuildRequest(requestMessageBuilder); } /// /// Constructs a HttpRequestMessage using bucket/object names from Args. /// Calls overloaded CreateRequest method. /// /// /// The direct descendant of ObjectArgs class, args with populated values from Input /// A HttpRequestMessage internal static async Task CreateRequest(this IMinioClient minioClient, ObjectArgs args) where T : ObjectArgs { ArgsCheck(args); var contentType = "application/octet-stream"; _ = args.Headers?.TryGetValue("Content-Type", out contentType); var requestMessageBuilder = await minioClient.CreateRequest(args.RequestMethod, args.BucketName, args.ObjectName, args.Headers, contentType, args.RequestBody).ConfigureAwait(false); return args.BuildRequest(requestMessageBuilder); } /// /// Constructs an HttpRequestMessage builder. For AWS, this function /// has the side-effect of overriding the baseUrl in the HttpClient /// with region specific host path or virtual style path. /// /// /// HTTP method /// Bucket Name /// Object Name /// headerMap /// Content Type /// request body /// query string /// boolean to define bucket creation /// A HttpRequestMessage builder /// When bucketName is invalid internal static async Task CreateRequest(this IMinioClient minioClient, HttpMethod method, string bucketName = null, string objectName = null, IDictionary headerMap = null, string contentType = "application/octet-stream", ReadOnlyMemory body = default, string resourcePath = null, bool isBucketCreationRequest = false) { var region = string.Empty; if (bucketName is not null) { Utils.ValidateBucketName(bucketName); // Fetch correct region for bucket if this is not a bucket creation if (!isBucketCreationRequest) region = await minioClient.GetRegion(bucketName).ConfigureAwait(false); } if (objectName is not null) Utils.ValidateObjectName(objectName); if (minioClient.Config.Provider is not null) { var isAWSEnvProvider = minioClient.Config.Provider is AWSEnvironmentProvider || (minioClient.Config.Provider is ChainedProvider ch && ch.CurrentProvider is AWSEnvironmentProvider); var isIAMAWSProvider = minioClient.Config.Provider is IAMAWSProvider || (minioClient.Config.Provider is ChainedProvider chained && chained.CurrentProvider is AWSEnvironmentProvider); AccessCredentials creds; if (isAWSEnvProvider) { var aWSEnvProvider = (AWSEnvironmentProvider)minioClient.Config.Provider; creds = await aWSEnvProvider.GetCredentialsAsync().ConfigureAwait(false); } else if (isIAMAWSProvider) { var iamAWSProvider = (IAMAWSProvider)minioClient.Config.Provider; creds = iamAWSProvider.Credentials; } else { creds = await minioClient.Config.Provider.GetCredentialsAsync().ConfigureAwait(false); } if (creds is not null) { minioClient.Config.AccessKey = creds.AccessKey; minioClient.Config.SecretKey = creds.SecretKey; } } // This section reconstructs the url with scheme followed by location specific endpoint (s3.region.amazonaws.com) // or Virtual Host styled endpoint (bucketname.s3.region.amazonaws.com) for Amazon requests. var resource = string.Empty; var usePathStyle = false; if (!string.IsNullOrEmpty(bucketName) && S3utils.IsAmazonEndPoint(minioClient.Config.BaseUrl)) { if (method == HttpMethod.Put && objectName is null && resourcePath is null) // use path style for make bucket to workaround "AuthorizationHeaderMalformed" error from s3.amazonaws.com usePathStyle = true; else if (resourcePath?.Contains("location", StringComparison.OrdinalIgnoreCase) == true) // use path style for location query usePathStyle = true; else if (bucketName.Contains('.', StringComparison.Ordinal) && minioClient.Config.Secure) // use path style where '.' in bucketName causes SSL certificate validation error usePathStyle = true; if (usePathStyle) resource += Utils.UrlEncode(bucketName) + "/"; } // Set Target URL var requestUrl = RequestUtil.MakeTargetURL(minioClient.Config.BaseUrl, minioClient.Config.Secure, bucketName, region, usePathStyle); if (objectName is not null) resource += Utils.EncodePath(objectName); // Append query string passed in if (resourcePath is not null) resource += resourcePath; HttpRequestMessageBuilder messageBuilder; if (!string.IsNullOrEmpty(resource)) messageBuilder = new HttpRequestMessageBuilder(method, requestUrl, resource); else messageBuilder = new HttpRequestMessageBuilder(method, requestUrl); if (!body.IsEmpty) { messageBuilder.SetBody(body); messageBuilder.AddOrUpdateHeaderParameter("Content-Type", contentType); } if (headerMap is not null) { if (headerMap.TryGetValue(messageBuilder.ContentTypeKey, out var value) && !string.IsNullOrEmpty(value)) headerMap[messageBuilder.ContentTypeKey] = contentType; foreach (var entry in headerMap) messageBuilder.AddOrUpdateHeaderParameter(entry.Key, entry.Value); } return messageBuilder; } /// /// Null Check for Args object. /// Expected to be called from CreateRequest /// /// The child object of Args class private static void ArgsCheck(RequestArgs args) { if (args is null) throw new ArgumentNullException(nameof(args), "Args object cannot be null. It needs to be assigned to an instantiated child object of Args."); } /// /// Resolve region of the bucket. /// /// /// /// internal static async Task GetRegion(this IMinioClient minioClient, string bucketName) { var rgn = ""; // Use user specified region in client constructor if present if (!string.IsNullOrEmpty(minioClient.Config.Region)) return minioClient.Config.Region; // pick region from endpoint if present if (!string.IsNullOrEmpty(minioClient.Config.Endpoint)) rgn = RegionHelper.GetRegionFromEndpoint(minioClient.Config.Endpoint); // Pick region from location HEAD request if (rgn?.Length == 0) rgn = BucketRegionCache.Instance.Exists(bucketName) ? await BucketRegionCache.Update(minioClient, bucketName).ConfigureAwait(false) : BucketRegionCache.Instance.Region(bucketName); // Defaults to us-east-1 if region could not be found return rgn?.Length == 0 ? "us-east-1" : rgn; } /// /// Delegate errors to handlers /// /// /// /// /// private static void HandleIfErrorResponse(this IMinioClient minioClient, ResponseResult response, IEnumerable handlers, DateTime startTime) { // Logs Response if HTTP tracing is enabled if (minioClient.Config.TraceHttp) { var now = DateTime.Now; minioClient.LogRequest(response.Request, response, (now - startTime).TotalMilliseconds); } if (handlers is null) throw new ArgumentNullException(nameof(handlers)); // Run through handlers passed to take up error handling foreach (var handler in handlers) handler.Handle(response); // Fall back default error handler minioClient.DefaultErrorHandler.Handle(response); } }