You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

367 lines
16 KiB

  1. using System.Diagnostics;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Net;
  4. using Minio.Credentials;
  5. using Minio.DataModel;
  6. using Minio.DataModel.Args;
  7. using Minio.DataModel.Result;
  8. using Minio.Exceptions;
  9. using Minio.Handlers;
  10. using Minio.Helper;
  11. namespace Minio;
  12. public static class RequestExtensions
  13. {
  14. [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings",
  15. Justification = "This is done in the interface. String is provided here for convenience")]
  16. public static Task<HttpResponseMessage> WrapperGetAsync(this IMinioClient minioClient, string url)
  17. {
  18. return minioClient is null
  19. ? throw new ArgumentNullException(nameof(minioClient))
  20. : minioClient.WrapperGetAsync(new Uri(url));
  21. }
  22. /// <summary>
  23. /// Runs httpClient's PutObjectAsync method
  24. /// </summary>
  25. [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings",
  26. Justification = "This is done in the interface. String is provided here for convenience")]
  27. public static Task WrapperPutAsync(this IMinioClient minioClient, string url, StreamContent strm)
  28. {
  29. return minioClient is null
  30. ? throw new ArgumentNullException(nameof(minioClient))
  31. : minioClient.WrapperPutAsync(new Uri(url), strm);
  32. }
  33. /// <summary>
  34. /// Actual doer that executes the request on the server
  35. /// </summary>
  36. /// <param name="minioClient"></param>
  37. /// <param name="requestMessageBuilder">The build of HttpRequestMessageBuilder </param>
  38. /// <param name="ignoreExceptionType">any type of Exception; if an exception type is going to be ignored</param>
  39. /// <param name="isSts">boolean; if true role credentials, otherwise IAM user</param>
  40. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  41. /// <returns>ResponseResult</returns>
  42. internal static async Task<ResponseResult> ExecuteTaskAsync(this IMinioClient minioClient,
  43. HttpRequestMessageBuilder requestMessageBuilder,
  44. Type ignoreExceptionType = null,
  45. bool isSts = false,
  46. CancellationToken cancellationToken = default)
  47. {
  48. var startTime = DateTime.Now;
  49. var responseResult = new ResponseResult(requestMessageBuilder.Request, response: null);
  50. using var internalTokenSource =
  51. new CancellationTokenSource(new TimeSpan(0, 0, 0, 0, minioClient.Config.RequestTimeout));
  52. using var timeoutTokenSource =
  53. CancellationTokenSource.CreateLinkedTokenSource(internalTokenSource.Token, cancellationToken);
  54. if (minioClient.Config.RequestTimeout > 0) cancellationToken = timeoutTokenSource.Token;
  55. responseResult = await minioClient.ExecuteWithRetry(
  56. async Task<ResponseResult> () => await minioClient.ExecuteTaskCoreAsync(
  57. requestMessageBuilder,
  58. isSts, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
  59. if ((responseResult is not null &&
  60. !Equals(responseResult.Exception?.GetType(), ignoreExceptionType)) ||
  61. responseResult.StatusCode != HttpStatusCode.OK)
  62. {
  63. var handler = new DefaultErrorHandler();
  64. handler.Handle(responseResult);
  65. }
  66. return responseResult;
  67. }
  68. private static async Task<ResponseResult> ExecuteTaskCoreAsync(this IMinioClient minioClient,
  69. // IEnumerable<IApiResponseErrorHandler> errorHandlers,
  70. HttpRequestMessageBuilder requestMessageBuilder,
  71. bool isSts = false,
  72. CancellationToken cancellationToken = default)
  73. {
  74. var startTime = Stopwatch.GetTimestamp();
  75. var v4Authenticator = new V4Authenticator(minioClient.Config.Secure,
  76. minioClient.Config.AccessKey, minioClient.Config.SecretKey, minioClient.Config.Region,
  77. minioClient.Config.SessionToken);
  78. requestMessageBuilder.AddOrUpdateHeaderParameter("Authorization",
  79. v4Authenticator.Authenticate(requestMessageBuilder, isSts));
  80. var request = requestMessageBuilder.Request;
  81. var responseResult = new ResponseResult(request, new HttpResponseMessage());
  82. try
  83. {
  84. var response = await minioClient.Config.HttpClient.SendAsync(request,
  85. HttpCompletionOption.ResponseHeadersRead,
  86. cancellationToken).ConfigureAwait(false);
  87. responseResult = new ResponseResult(request, response);
  88. if (requestMessageBuilder.ResponseWriter is not null)
  89. await requestMessageBuilder.ResponseWriter(responseResult.ContentStream, cancellationToken)
  90. .ConfigureAwait(false);
  91. }
  92. catch (Exception ex)
  93. {
  94. responseResult.Exception = ex;
  95. }
  96. return responseResult;
  97. }
  98. private static async Task<ResponseResult> ExecuteWithRetry(this IMinioClient minioClient,
  99. Func<Task<ResponseResult>> executeRequestCallback)
  100. {
  101. return minioClient.Config.RetryPolicyHandler is null
  102. ? await executeRequestCallback().ConfigureAwait(false)
  103. : await minioClient.Config.RetryPolicyHandler.Handle(executeRequestCallback).ConfigureAwait(false);
  104. }
  105. /// <summary>
  106. /// Constructs a HttpRequestMessageBuilder using bucket/object names from Args.
  107. /// Calls overloaded CreateRequest method.
  108. /// </summary>
  109. /// <param name="minioClient"></param>
  110. /// <param name="args">The direct descendant of BucketArgs class, args with populated values from Input</param>
  111. /// <returns>A HttpRequestMessageBuilder</returns>
  112. internal static async Task<HttpRequestMessageBuilder> CreateRequest<T>(this IMinioClient minioClient,
  113. BucketArgs<T> args) where T : BucketArgs<T>
  114. {
  115. ArgsCheck(args);
  116. var requestMessageBuilder =
  117. await minioClient.CreateRequest(args.RequestMethod, args.BucketName, headerMap: args.Headers,
  118. isBucketCreationRequest: args.IsBucketCreationRequest).ConfigureAwait(false);
  119. return args.BuildRequest(requestMessageBuilder);
  120. }
  121. /// <summary>
  122. /// Constructs a HttpRequestMessage using bucket/object names from Args.
  123. /// Calls overloaded CreateRequest method.
  124. /// </summary>
  125. /// <param name="minioClient"></param>
  126. /// <param name="args">The direct descendant of ObjectArgs class, args with populated values from Input</param>
  127. /// <returns>A HttpRequestMessage</returns>
  128. internal static async Task<HttpRequestMessageBuilder> CreateRequest<T>(this IMinioClient minioClient,
  129. ObjectArgs<T> args) where T : ObjectArgs<T>
  130. {
  131. ArgsCheck(args);
  132. var contentType = "application/octet-stream";
  133. _ = args.Headers?.TryGetValue("Content-Type", out contentType);
  134. var requestMessageBuilder =
  135. await minioClient.CreateRequest(args.RequestMethod,
  136. args.BucketName,
  137. args.ObjectName,
  138. args.Headers,
  139. contentType,
  140. args.RequestBody).ConfigureAwait(false);
  141. return args.BuildRequest(requestMessageBuilder);
  142. }
  143. /// <summary>
  144. /// Constructs an HttpRequestMessage builder. For AWS, this function
  145. /// has the side-effect of overriding the baseUrl in the HttpClient
  146. /// with region specific host path or virtual style path.
  147. /// </summary>
  148. /// <param name="minioClient"></param>
  149. /// <param name="method">HTTP method</param>
  150. /// <param name="bucketName">Bucket Name</param>
  151. /// <param name="objectName">Object Name</param>
  152. /// <param name="headerMap">headerMap</param>
  153. /// <param name="contentType">Content Type</param>
  154. /// <param name="body">request body</param>
  155. /// <param name="resourcePath">query string</param>
  156. /// <param name="isBucketCreationRequest">boolean to define bucket creation</param>
  157. /// <returns>A HttpRequestMessage builder</returns>
  158. /// <exception cref="BucketNotFoundException">When bucketName is invalid</exception>
  159. internal static async Task<HttpRequestMessageBuilder> CreateRequest(this IMinioClient minioClient,
  160. HttpMethod method,
  161. string bucketName = null,
  162. string objectName = null,
  163. IDictionary<string, string> headerMap = null,
  164. string contentType = "application/octet-stream",
  165. ReadOnlyMemory<byte> body = default,
  166. string resourcePath = null,
  167. bool isBucketCreationRequest = false)
  168. {
  169. var region = string.Empty;
  170. if (bucketName is not null)
  171. {
  172. Utils.ValidateBucketName(bucketName);
  173. // Fetch correct region for bucket if this is not a bucket creation
  174. if (!isBucketCreationRequest)
  175. region = await minioClient.GetRegion(bucketName).ConfigureAwait(false);
  176. }
  177. if (objectName is not null) Utils.ValidateObjectName(objectName);
  178. if (minioClient.Config.Provider is not null)
  179. {
  180. var isAWSEnvProvider = minioClient.Config.Provider is AWSEnvironmentProvider ||
  181. (minioClient.Config.Provider is ChainedProvider ch &&
  182. ch.CurrentProvider is AWSEnvironmentProvider);
  183. var isIAMAWSProvider = minioClient.Config.Provider is IAMAWSProvider ||
  184. (minioClient.Config.Provider is ChainedProvider chained &&
  185. chained.CurrentProvider is AWSEnvironmentProvider);
  186. AccessCredentials creds;
  187. if (isAWSEnvProvider)
  188. {
  189. var aWSEnvProvider = (AWSEnvironmentProvider)minioClient.Config.Provider;
  190. creds = await aWSEnvProvider.GetCredentialsAsync().ConfigureAwait(false);
  191. }
  192. else if (isIAMAWSProvider)
  193. {
  194. var iamAWSProvider = (IAMAWSProvider)minioClient.Config.Provider;
  195. creds = iamAWSProvider.Credentials;
  196. }
  197. else
  198. {
  199. creds = await minioClient.Config.Provider.GetCredentialsAsync().ConfigureAwait(false);
  200. }
  201. if (creds is not null)
  202. {
  203. minioClient.Config.AccessKey = creds.AccessKey;
  204. minioClient.Config.SecretKey = creds.SecretKey;
  205. }
  206. }
  207. // This section reconstructs the url with scheme followed by location specific endpoint (s3.region.amazonaws.com)
  208. // or Virtual Host styled endpoint (bucketname.s3.region.amazonaws.com) for Amazon requests.
  209. var resource = string.Empty;
  210. var usePathStyle = false;
  211. if (!string.IsNullOrEmpty(bucketName) && S3utils.IsAmazonEndPoint(minioClient.Config.BaseUrl))
  212. {
  213. if (method == HttpMethod.Put && objectName is null && resourcePath is null)
  214. // use path style for make bucket to workaround "AuthorizationHeaderMalformed" error from s3.amazonaws.com
  215. usePathStyle = true;
  216. else if (resourcePath?.Contains("location", StringComparison.OrdinalIgnoreCase) == true)
  217. // use path style for location query
  218. usePathStyle = true;
  219. else if (bucketName.Contains('.', StringComparison.Ordinal) && minioClient.Config.Secure)
  220. // use path style where '.' in bucketName causes SSL certificate validation error
  221. usePathStyle = true;
  222. if (usePathStyle) resource += Utils.UrlEncode(bucketName) + "/";
  223. }
  224. // Set Target URL
  225. var requestUrl = RequestUtil.MakeTargetURL(minioClient.Config.BaseUrl, minioClient.Config.Secure, bucketName,
  226. region, usePathStyle);
  227. if (objectName is not null) resource += Utils.EncodePath(objectName);
  228. // Append query string passed in
  229. if (resourcePath is not null) resource += resourcePath;
  230. HttpRequestMessageBuilder messageBuilder;
  231. if (!string.IsNullOrEmpty(resource))
  232. messageBuilder = new HttpRequestMessageBuilder(method, requestUrl, resource);
  233. else
  234. messageBuilder = new HttpRequestMessageBuilder(method, requestUrl);
  235. if (!body.IsEmpty)
  236. {
  237. messageBuilder.SetBody(body);
  238. messageBuilder.AddOrUpdateHeaderParameter("Content-Type", contentType);
  239. }
  240. if (headerMap?.Count > 0)
  241. {
  242. if (headerMap.TryGetValue(messageBuilder.ContentTypeKey, out var value) && !string.IsNullOrEmpty(value))
  243. headerMap[messageBuilder.ContentTypeKey] = contentType;
  244. foreach (var entry in headerMap) messageBuilder.AddOrUpdateHeaderParameter(entry.Key, entry.Value);
  245. }
  246. return messageBuilder;
  247. }
  248. /// <summary>
  249. /// Null Check for Args object.
  250. /// Expected to be called from CreateRequest
  251. /// </summary>
  252. /// <param name="args">The child object of Args class</param>
  253. private static void ArgsCheck(RequestArgs args)
  254. {
  255. if (args is null)
  256. throw new ArgumentNullException(nameof(args),
  257. "Args object cannot be null. It needs to be assigned to an instantiated child object of Args.");
  258. }
  259. /// <summary>
  260. /// Resolve region of the bucket.
  261. /// </summary>
  262. /// <param name="minioClient"></param>
  263. /// <param name="bucketName"></param>
  264. /// <returns></returns>
  265. internal static async Task<string> GetRegion(this IMinioClient minioClient, string bucketName)
  266. {
  267. var rgn = "";
  268. // Use user specified region in client constructor if present
  269. if (!string.IsNullOrEmpty(minioClient.Config.Region)) return minioClient.Config.Region;
  270. // pick region from endpoint if present
  271. if (!string.IsNullOrEmpty(minioClient.Config.Endpoint))
  272. rgn = RegionHelper.GetRegionFromEndpoint(minioClient.Config.Endpoint);
  273. // Pick region from location HEAD request
  274. if (rgn?.Length == 0)
  275. rgn = BucketRegionCache.Instance.Exists(bucketName)
  276. ? await BucketRegionCache.Update(minioClient, bucketName).ConfigureAwait(false)
  277. : BucketRegionCache.Instance.Region(bucketName);
  278. // Defaults to us-east-1 if region could not be found
  279. return rgn?.Length == 0 ? "us-east-1" : rgn;
  280. }
  281. /// <summary>
  282. /// Delegate errors to handlers
  283. /// </summary>
  284. /// <param name="minioClient"></param>
  285. /// <param name="response"></param>
  286. /// <param name="handlers"></param>
  287. /// <param name="startTime"></param>
  288. /// <param name="ignoreExceptionType"></param>
  289. private static void HandleIfErrorResponse(this IMinioClient minioClient, ResponseResult response,
  290. IEnumerable<IApiResponseErrorHandler> handlers,
  291. long startTime,
  292. Type ignoreExceptionType = null)
  293. {
  294. // Logs Response if HTTP tracing is enabled
  295. if (minioClient.Config.TraceHttp)
  296. {
  297. var elapsed = GetElapsedTime(startTime);
  298. minioClient.LogRequest(response.Request, response, elapsed.TotalMilliseconds);
  299. }
  300. if (response.Exception is not null)
  301. {
  302. if (response.Exception?.GetType() == ignoreExceptionType)
  303. {
  304. response.Exception = null;
  305. }
  306. else
  307. {
  308. if (handlers.Any())
  309. // Run through handlers passed to take up error handling
  310. foreach (var handler in handlers)
  311. handler.Handle(response);
  312. else
  313. minioClient.DefaultErrorHandler.Handle(response);
  314. }
  315. }
  316. }
  317. private static TimeSpan GetElapsedTime(long startTimestamp)
  318. {
  319. #if NET8_0_OR_GREATER
  320. return Stopwatch.GetElapsedTime(startTimestamp);
  321. #else
  322. var endTimestamp = Stopwatch.GetTimestamp();
  323. var elapsedTicks = endTimestamp - startTimestamp;
  324. var seconds = (double)elapsedTicks / Stopwatch.Frequency;
  325. return TimeSpan.FromSeconds(seconds);
  326. #endif
  327. }
  328. }