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.
786 lines
31 KiB
786 lines
31 KiB
/*
|
|
* MinIO .NET Library for Amazon S3 Compatible Cloud Storage,
|
|
* (C) 2017, 2018, 2019, 2020 MinIO, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Web;
|
|
using System.Xml.Serialization;
|
|
|
|
using Minio.DataModel.Tracing;
|
|
using Minio.Exceptions;
|
|
using Minio.Helper;
|
|
|
|
using RestSharp;
|
|
|
|
namespace Minio
|
|
{
|
|
public partial class MinioClient
|
|
{
|
|
// Save Credentials from user
|
|
internal string AccessKey { get; private set; }
|
|
internal string SecretKey { get; private set; }
|
|
internal string BaseUrl { get; private set; }
|
|
|
|
// Reconstructed endpoint with scheme and host.In the case of Amazon, this url
|
|
// is the virtual style path or location based endpoint
|
|
internal string Endpoint { get; private set; }
|
|
internal string Region;
|
|
internal string SessionToken { get; private set; }
|
|
// Corresponding URI for above endpoint
|
|
internal Uri uri;
|
|
|
|
// Indicates if we are using HTTPS or not
|
|
internal bool Secure { get; private set; }
|
|
|
|
// RESTSharp client
|
|
internal IRestClient restClient;
|
|
// Custom authenticator for RESTSharp
|
|
internal V4Authenticator authenticator;
|
|
// Handler for task retry policy
|
|
internal RetryPolicyHandlingDelegate retryPolicyHandler;
|
|
|
|
// Cache holding bucket to region mapping for buckets seen so far.
|
|
internal BucketRegionCache regionCache;
|
|
|
|
private IRequestLogger logger;
|
|
|
|
// Enables HTTP tracing if set to true
|
|
private bool trace = false;
|
|
|
|
private const string RegistryAuthHeaderKey = "X-Registry-Auth";
|
|
|
|
internal readonly IEnumerable<ApiResponseErrorHandlingDelegate> NoErrorHandlers = Enumerable.Empty<ApiResponseErrorHandlingDelegate>();
|
|
|
|
/// <summary>
|
|
/// Default error handling delegate
|
|
/// </summary>
|
|
private readonly ApiResponseErrorHandlingDelegate _defaultErrorHandlingDelegate = (response) =>
|
|
{
|
|
if (response.StatusCode < HttpStatusCode.OK || response.StatusCode >= HttpStatusCode.BadRequest)
|
|
{
|
|
ParseError(response);
|
|
}
|
|
};
|
|
|
|
private static string SystemUserAgent
|
|
{
|
|
get
|
|
{
|
|
string release = "minio-dotnet/1.0.9";
|
|
#if NET46
|
|
string arch = Environment.Is64BitOperatingSystem ? "x86_64" : "x86";
|
|
return $"MinIO ({Environment.OSVersion};{arch}) {release}";
|
|
#else
|
|
string arch = RuntimeInformation.OSArchitecture.ToString();
|
|
return $"MinIO ({RuntimeInformation.OSDescription};{arch}) {release}";
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private string CustomUserAgent = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Returns the User-Agent header for the request
|
|
/// </summary>
|
|
private string FullUserAgent
|
|
{
|
|
get
|
|
{
|
|
return $"{SystemUserAgent} {CustomUserAgent}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve region bucket resides in.
|
|
/// </summary>
|
|
/// <param name="bucketName"></param>
|
|
/// <returns></returns>
|
|
private async Task<string> GetRegion(string bucketName)
|
|
{
|
|
// Use user specified region in client constructor if present
|
|
if (this.Region != string.Empty)
|
|
{
|
|
return this.Region;
|
|
}
|
|
|
|
// pick region from endpoint if present
|
|
string region = Regions.GetRegionFromEndpoint(this.Endpoint);
|
|
|
|
// Pick region from location HEAD request
|
|
if (region == string.Empty)
|
|
{
|
|
if (!BucketRegionCache.Instance.Exists(bucketName))
|
|
{
|
|
region = await BucketRegionCache.Instance.Update(this, bucketName).ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
region = BucketRegionCache.Instance.Region(bucketName);
|
|
}
|
|
}
|
|
// Default to us-east-1 if region could not be found
|
|
return (region == string.Empty) ? "us-east-1" : region;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Null Check for Args object.
|
|
/// Expected to be called from CreateRequest
|
|
/// </summary>
|
|
/// <param name="args">The child object of Args class</param>
|
|
private void ArgsCheck(Args 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.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructs a RestRequest using bucket/object names from Args.
|
|
/// Calls overloaded CreateRequest method.
|
|
/// </summary>
|
|
/// <param name="args">The direct descendant of BucketArgs class, args with populated values from Input</param>
|
|
/// <returns>A RestRequest</returns>
|
|
internal async Task<RestRequest> CreateRequest<T>(BucketArgs<T> args) where T : BucketArgs<T>
|
|
{
|
|
this.ArgsCheck(args);
|
|
RestRequest request = await this.CreateRequest(args.RequestMethod, args.BucketName).ConfigureAwait(false);
|
|
return args.BuildRequest(request);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Constructs a RestRequest using bucket/object names from Args.
|
|
/// Calls overloaded CreateRequest method.
|
|
/// </summary>
|
|
/// <param name="args">The direct descendant of ObjectArgs class, args with populated values from Input</param>
|
|
/// <returns>A RestRequest</returns>
|
|
internal async Task<RestRequest> CreateRequest<T>(ObjectArgs<T> args) where T : ObjectArgs<T>
|
|
{
|
|
this.ArgsCheck(args);
|
|
string contentType = "application/octet-stream";
|
|
args.HeaderMap?.TryGetValue("Content-Type", out contentType);
|
|
RestRequest request = await this.CreateRequest(args.RequestMethod,
|
|
args.BucketName,
|
|
args.ObjectName,
|
|
args.HeaderMap,
|
|
contentType,
|
|
args.RequestBody,
|
|
null).ConfigureAwait(false);
|
|
return args.BuildRequest(request);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Constructs a RestRequest. For AWS, this function has the side-effect of overriding the baseUrl
|
|
/// in the RestClient with region specific host path or virtual style path.
|
|
/// </summary>
|
|
/// <param name="method">HTTP method</param>
|
|
/// <param name="bucketName">Bucket Name</param>
|
|
/// <param name="objectName">Object Name</param>
|
|
/// <param name="headerMap">headerMap</param>
|
|
/// <param name="contentType">Content Type</param>
|
|
/// <param name="body">request body</param>
|
|
/// <param name="resourcePath">query string</param>
|
|
/// <returns>A RestRequest</returns>
|
|
/// <exception cref="BucketNotFoundException">When bucketName is invalid</exception>
|
|
internal async Task<RestRequest> CreateRequest(Method method, string bucketName = null, string objectName = null,
|
|
Dictionary<string, string> headerMap = null,
|
|
string contentType = "application/octet-stream",
|
|
object body = null, string resourcePath = null)
|
|
{
|
|
string region = string.Empty;
|
|
if (bucketName != null)
|
|
{
|
|
utils.ValidateBucketName(bucketName);
|
|
region = await GetRegion(bucketName).ConfigureAwait(false);
|
|
}
|
|
|
|
if (objectName != null)
|
|
{
|
|
utils.ValidateObjectName(objectName);
|
|
}
|
|
|
|
// Start with user specified endpoint
|
|
string host = this.BaseUrl;
|
|
|
|
this.restClient.Authenticator = new V4Authenticator(this.Secure, this.AccessKey, this.SecretKey, region: string.IsNullOrEmpty(this.Region)?region:this.Region, sessionToken: this.SessionToken);
|
|
|
|
// 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.
|
|
string resource = string.Empty;
|
|
bool usePathStyle = false;
|
|
if (bucketName != null)
|
|
{
|
|
if (s3utils.IsAmazonEndPoint(this.BaseUrl))
|
|
{
|
|
usePathStyle = false;
|
|
|
|
if (method == Method.PUT && objectName == null && resourcePath == null)
|
|
{
|
|
// use path style for make bucket to workaround "AuthorizationHeaderMalformed" error from s3.amazonaws.com
|
|
usePathStyle = true;
|
|
}
|
|
else if (resourcePath != null && resourcePath.Contains("location"))
|
|
{
|
|
// use path style for location query
|
|
usePathStyle = true;
|
|
}
|
|
else if (bucketName != null && bucketName.Contains(".") && this.Secure)
|
|
{
|
|
// use path style where '.' in bucketName causes SSL certificate validation error
|
|
usePathStyle = true;
|
|
}
|
|
|
|
if (usePathStyle)
|
|
{
|
|
resource += utils.UrlEncode(bucketName) + "/";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
resource += utils.UrlEncode(bucketName) + "/";
|
|
}
|
|
}
|
|
|
|
// Set Target URL
|
|
Uri requestUrl = RequestUtil.MakeTargetURL(this.BaseUrl, this.Secure, bucketName, region, usePathStyle);
|
|
SetTargetURL(requestUrl);
|
|
|
|
if (objectName != null)
|
|
{
|
|
resource += utils.EncodePath(objectName);
|
|
}
|
|
|
|
// Append query string passed in
|
|
if (resourcePath != null)
|
|
{
|
|
resource += resourcePath;
|
|
}
|
|
|
|
RestRequest request = new RestRequest(resource, method);
|
|
|
|
if (body != null)
|
|
{
|
|
request.AddParameter(contentType, body, RestSharp.ParameterType.RequestBody);
|
|
}
|
|
|
|
if (headerMap != null)
|
|
{
|
|
foreach (var entry in headerMap)
|
|
{
|
|
request.AddHeader(entry.Key, entry.Value);
|
|
}
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// The Init method used with MinioClient constructor with multiple arguments. The host URI for Amazon is set to virtual hosted style
|
|
/// if usePathStyle is false. Otherwise path style URL is constructed.
|
|
/// </summary>
|
|
internal void InitClient()
|
|
{
|
|
if (string.IsNullOrEmpty(this.BaseUrl))
|
|
{
|
|
throw new InvalidEndpointException("Endpoint cannot be empty.");
|
|
}
|
|
else if (this.Secure && this.restClient != null && this.restClient.BaseUrl == null)
|
|
{
|
|
Uri secureUrl = RequestUtil.MakeTargetURL(this.BaseUrl, this.Secure);
|
|
this.SetTargetURL(secureUrl);
|
|
}
|
|
string host = this.BaseUrl;
|
|
|
|
var scheme = this.Secure ? utils.UrlEncode("https") : utils.UrlEncode("http");
|
|
// This is the actual url pointed to for all HTTP requests
|
|
this.Endpoint = string.Format("{0}://{1}", scheme, host);
|
|
Init();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method initializes a new RESTClient. It is called by other Inits
|
|
/// </summary>
|
|
|
|
internal void Init()
|
|
{
|
|
this.uri = RequestUtil.GetEndpointURL(this.BaseUrl, this.Secure);
|
|
RequestUtil.ValidateEndpoint(this.uri, this.Endpoint);
|
|
|
|
// Initialize a new REST client. This uri will be modified if region specific endpoint/virtual style request
|
|
// is decided upon while constructing a request for Amazon.
|
|
restClient = new RestSharp.RestClient(this.uri)
|
|
{
|
|
UserAgent = this.FullUserAgent
|
|
};
|
|
|
|
authenticator = new V4Authenticator(this.Secure, this.AccessKey, this.SecretKey, this.Region, this.SessionToken);
|
|
restClient.Authenticator = authenticator;
|
|
restClient.UseUrlEncoder(s => HttpUtility.UrlEncode(s));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets app version and name. Used by RestSharp for constructing User-Agent header in all HTTP requests
|
|
/// </summary>
|
|
/// <param name="appName"></param>
|
|
/// <param name="appVersion"></param>
|
|
public void SetAppInfo(string appName, string appVersion)
|
|
{
|
|
if (string.IsNullOrEmpty(appName))
|
|
{
|
|
throw new ArgumentException("Appname cannot be null or empty", nameof(appName));
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(appVersion))
|
|
{
|
|
throw new ArgumentException("Appversion cannot be null or empty", nameof(appVersion));
|
|
}
|
|
|
|
this.CustomUserAgent = $"{appName}/{appVersion}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns an Cloud Storage client
|
|
/// </summary>
|
|
/// <returns>Client with no arguments to be used with other builder methods</returns>
|
|
public MinioClient()
|
|
{
|
|
this.Region = "";
|
|
this.SessionToken = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns an Cloud Storage client
|
|
/// </summary>
|
|
/// <param name="endpoint">Location of the server, supports HTTP and HTTPS</param>
|
|
/// <param name="accessKey">Access Key for authenticated requests (Optional, can be omitted for anonymous requests)</param>
|
|
/// <param name="secretKey">Secret Key for authenticated requests (Optional, can be omitted for anonymous requests)</param>
|
|
/// <param name="region">Optional custom region</param>
|
|
/// <param name="sessionToken">Optional session token</param>
|
|
/// <returns>Client initialized with user credentials</returns>
|
|
[Obsolete("Use appropriate Builder object and call Build() or BuildAsync()")]
|
|
public MinioClient(string endpoint, string accessKey = "", string secretKey = "", string region = "", string sessionToken = "")
|
|
{
|
|
this.Secure = false;
|
|
|
|
// Save user entered credentials
|
|
this.BaseUrl = endpoint;
|
|
this.AccessKey = accessKey;
|
|
this.SecretKey = secretKey;
|
|
this.SessionToken = sessionToken;
|
|
this.Region = region;
|
|
// Instantiate a region cache
|
|
this.regionCache = BucketRegionCache.Instance;
|
|
|
|
this.InitClient();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Connects to Cloud Storage with HTTPS if this method is invoked on client object
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public MinioClient WithSSL()
|
|
{
|
|
this.Secure = true;
|
|
if (string.IsNullOrEmpty(this.BaseUrl))
|
|
{
|
|
return this;
|
|
}
|
|
Uri secureUrl = RequestUtil.MakeTargetURL(this.BaseUrl, this.Secure);
|
|
this.SetTargetURL(secureUrl);
|
|
return this;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Uses webproxy for all requests if this method is invoked on client object
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public MinioClient WithProxy(IWebProxy proxy)
|
|
{
|
|
this.restClient.Proxy = proxy;
|
|
this.Proxy = proxy;
|
|
return this;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Uses the set timeout for all requests if this method is invoked on client object
|
|
/// </summary>
|
|
/// <param name="timeout">Timeout in milliseconds.</param>
|
|
/// <returns></returns>
|
|
public MinioClient WithTimeout(int timeout)
|
|
{
|
|
this.restClient.Timeout = timeout;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows to add retry policy handler
|
|
/// </summary>
|
|
/// <param name="retryPolicyHandler">Delegate that will wrap execution of <see cref="IRestRequest"/> requests.</param>
|
|
/// <returns></returns>
|
|
public MinioClient WithRetryPolicy(RetryPolicyHandlingDelegate retryPolicyHandler)
|
|
{
|
|
this.retryPolicyHandler = retryPolicyHandler;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets endpoint URL on the client object that request will be made against
|
|
/// </summary>
|
|
internal void SetTargetURL(Uri uri)
|
|
{
|
|
if (this.restClient == null)
|
|
{
|
|
restClient = new RestSharp.RestClient(uri)
|
|
{
|
|
UserAgent = this.FullUserAgent
|
|
};
|
|
}
|
|
this.restClient.BaseUrl = uri;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Actual doer that executes the REST request to the server
|
|
/// </summary>
|
|
/// <param name="errorHandlers">List of handlers to override default handling</param>
|
|
/// <param name="request">request</param>
|
|
/// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
|
|
/// <returns>IRESTResponse</returns>
|
|
internal Task<IRestResponse> ExecuteAsync(IEnumerable<ApiResponseErrorHandlingDelegate> errorHandlers, IRestRequest request, CancellationToken cancellationToken = default(CancellationToken))
|
|
{
|
|
return ExecuteWithRetry(
|
|
() => ExecuteTaskCoreAsync(errorHandlers, request, cancellationToken));
|
|
}
|
|
|
|
private async Task<IRestResponse> ExecuteTaskCoreAsync(IEnumerable<ApiResponseErrorHandlingDelegate> errorHandlers, IRestRequest request, CancellationToken cancellationToken = default(CancellationToken))
|
|
{
|
|
var startTime = DateTime.Now;
|
|
// Logs full url when HTTPtracing is enabled.
|
|
if (this.trace)
|
|
{
|
|
var fullUrl = this.restClient.BuildUri(request);
|
|
Console.WriteLine($"Full URL of Request {fullUrl}");
|
|
}
|
|
|
|
IRestResponse response = await this.restClient.ExecuteAsync(request, request.Method, cancellationToken).ConfigureAwait(false);
|
|
|
|
this.HandleIfErrorResponse(response, errorHandlers, startTime);
|
|
return response;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse response errors if any and return relevant error messages
|
|
/// </summary>
|
|
/// <param name="response"></param>
|
|
internal static void ParseError(IRestResponse response)
|
|
{
|
|
if (response == null)
|
|
{
|
|
throw new ConnectionException("Response is nil. Please report this issue https://github.com/minio/minio-dotnet/issues", response);
|
|
}
|
|
|
|
if (HttpStatusCode.Redirect.Equals(response.StatusCode) || HttpStatusCode.TemporaryRedirect.Equals(response.StatusCode) || HttpStatusCode.MovedPermanently.Equals(response.StatusCode))
|
|
{
|
|
throw new RedirectionException("Redirection detected. Please report this issue https://github.com/minio/minio-dotnet/issues");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(response.Content))
|
|
{
|
|
ParseErrorNoContent(response);
|
|
return;
|
|
}
|
|
|
|
ParseErrorFromContent(response);
|
|
}
|
|
|
|
private static void ParseErrorNoContent(IRestResponse response)
|
|
{
|
|
if (HttpStatusCode.Forbidden.Equals(response.StatusCode)
|
|
|| HttpStatusCode.BadRequest.Equals(response.StatusCode)
|
|
|| HttpStatusCode.NotFound.Equals(response.StatusCode)
|
|
|| HttpStatusCode.MethodNotAllowed.Equals(response.StatusCode)
|
|
|| HttpStatusCode.NotImplemented.Equals(response.StatusCode))
|
|
{
|
|
ParseWellKnownErrorNoContent(response);
|
|
}
|
|
|
|
if (response.StatusCode == 0)
|
|
throw new ConnectionException("Connection error: " + response.ErrorMessage, response);
|
|
|
|
throw new InternalClientException("Unsuccessful response from server without XML error: " + response.ErrorMessage, response);
|
|
}
|
|
|
|
private static void ParseWellKnownErrorNoContent(IRestResponse response)
|
|
{
|
|
MinioException error = null;
|
|
ErrorResponse errorResponse = new ErrorResponse();
|
|
|
|
foreach (Parameter parameter in response.Headers)
|
|
{
|
|
if (parameter.Name.Equals("x-amz-id-2", StringComparison.CurrentCultureIgnoreCase))
|
|
{
|
|
errorResponse.HostId = parameter.Value.ToString();
|
|
}
|
|
|
|
if (parameter.Name.Equals("x-amz-request-id", StringComparison.CurrentCultureIgnoreCase))
|
|
{
|
|
errorResponse.RequestId = parameter.Value.ToString();
|
|
}
|
|
|
|
if (parameter.Name.Equals("x-amz-bucket-region", StringComparison.CurrentCultureIgnoreCase))
|
|
{
|
|
errorResponse.BucketRegion = parameter.Value.ToString();
|
|
}
|
|
}
|
|
|
|
errorResponse.Resource = response.Request.Resource;
|
|
|
|
// zero, one or two segments
|
|
var resourceSplits = response.Request.Resource.Split(new[] { '/' }, 2, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (HttpStatusCode.NotFound.Equals(response.StatusCode))
|
|
{
|
|
int pathLength = resourceSplits.Length;
|
|
bool isAWS = response.ResponseUri.Host.EndsWith("s3.amazonaws.com");
|
|
bool isVirtual = isAWS && !response.ResponseUri.Host.StartsWith("s3.amazonaws.com");
|
|
|
|
if (pathLength > 1)
|
|
{
|
|
var objectName = resourceSplits[1];
|
|
errorResponse.Code = "NoSuchKey";
|
|
error = new ObjectNotFoundException(objectName, "Not found.");
|
|
}
|
|
else if (pathLength == 1)
|
|
{
|
|
var resource = resourceSplits[0];
|
|
|
|
if (isAWS && isVirtual && response.Request.Resource != string.Empty)
|
|
{
|
|
errorResponse.Code = "NoSuchKey";
|
|
error = new ObjectNotFoundException(resource, "Not found.");
|
|
}
|
|
else
|
|
{
|
|
errorResponse.Code = "NoSuchBucket";
|
|
BucketRegionCache.Instance.Remove(resource);
|
|
error = new BucketNotFoundException(resource, "Not found.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
error = new InternalClientException("404 without body resulted in path with less than two components", response);
|
|
}
|
|
}
|
|
else if (HttpStatusCode.BadRequest.Equals(response.StatusCode))
|
|
{
|
|
int pathLength = resourceSplits.Length;
|
|
|
|
if (pathLength > 1)
|
|
{
|
|
var objectName = resourceSplits[1];
|
|
errorResponse.Code = "InvalidObjectName";
|
|
error = new InvalidObjectNameException(objectName, "Invalid object name.");
|
|
}
|
|
else
|
|
{
|
|
error = new InternalClientException("400 without body resulted in path with less than two components", response);
|
|
}
|
|
}
|
|
else if (HttpStatusCode.Forbidden.Equals(response.StatusCode))
|
|
{
|
|
errorResponse.Code = "Forbidden";
|
|
error = new AccessDeniedException("Access denied on the resource: " + response.Request.Resource);
|
|
}
|
|
|
|
error.Response = errorResponse;
|
|
throw error;
|
|
}
|
|
|
|
private static void ParseErrorFromContent(IRestResponse response)
|
|
{
|
|
if (response.StatusCode.Equals(HttpStatusCode.NotFound)
|
|
&& response.Request.Resource.EndsWith("?location")
|
|
&& response.Request.Method.Equals(Method.GET))
|
|
{
|
|
var bucketName = response.Request.Resource.Split('?')[0];
|
|
BucketRegionCache.Instance.Remove(bucketName);
|
|
throw new BucketNotFoundException(bucketName, "Not found.");
|
|
}
|
|
|
|
var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
|
|
var stream = new MemoryStream(contentBytes);
|
|
ErrorResponse errResponse = (ErrorResponse)new XmlSerializer(typeof(ErrorResponse)).Deserialize(stream);
|
|
|
|
if (response.StatusCode.Equals(HttpStatusCode.Forbidden)
|
|
&& (errResponse.Code.Equals("SignatureDoesNotMatch") || errResponse.Code.Equals("InvalidAccessKeyId")))
|
|
{
|
|
throw new AuthorizationException(errResponse.Resource, errResponse.BucketName, errResponse.Message);
|
|
}
|
|
|
|
// Handle XML response for Bucket Policy not found case
|
|
if (response.StatusCode.Equals(HttpStatusCode.NotFound)
|
|
&& response.Request.Resource.EndsWith("?policy")
|
|
&& response.Request.Method.Equals(Method.GET)
|
|
&& errResponse.Code == "NoSuchBucketPolicy")
|
|
{
|
|
throw new ErrorResponseException(errResponse, response)
|
|
{
|
|
XmlError = response.Content
|
|
};
|
|
}
|
|
|
|
if (response.StatusCode.Equals(HttpStatusCode.NotFound)
|
|
&& errResponse.Code == "NoSuchBucket")
|
|
{
|
|
throw new BucketNotFoundException(errResponse.BucketName, "Not found.");
|
|
}
|
|
|
|
if (response.StatusCode.Equals(HttpStatusCode.BadRequest)
|
|
&& errResponse.Code.Equals("MalformedXML"))
|
|
{
|
|
throw new MalFormedXMLException(errResponse.Resource, errResponse.BucketName, errResponse.Message, errResponse.Key);
|
|
}
|
|
|
|
if (response.StatusCode.Equals(HttpStatusCode.BadRequest)
|
|
&& errResponse.Code.Equals("InvalidRequest"))
|
|
{
|
|
Parameter param = new Parameter("legal-hold", "", ParameterType.QueryString);
|
|
if (response.Request.Parameters.Contains(param))
|
|
{
|
|
throw new MissingObjectLockConfiguration(errResponse.BucketName, errResponse.Message);
|
|
}
|
|
}
|
|
|
|
throw new UnexpectedMinioException(errResponse.Message)
|
|
{
|
|
Response = errResponse,
|
|
XmlError = response.Content
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delegate errors to handlers
|
|
/// </summary>
|
|
/// <param name="response"></param>
|
|
/// <param name="handlers"></param>
|
|
/// <param name="startTime"></param>
|
|
private void HandleIfErrorResponse(IRestResponse response, IEnumerable<ApiResponseErrorHandlingDelegate> handlers, DateTime startTime)
|
|
{
|
|
// Logs Response if HTTP tracing is enabled
|
|
if (this.trace)
|
|
{
|
|
DateTime now = DateTime.Now;
|
|
LogRequest(response.Request, response, (now - startTime).TotalMilliseconds);
|
|
}
|
|
|
|
if (handlers == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(handlers));
|
|
}
|
|
|
|
// Run through handlers passed to take up error handling
|
|
foreach (var handler in handlers)
|
|
{
|
|
handler(response);
|
|
}
|
|
|
|
// Fall back default error handler
|
|
_defaultErrorHandlingDelegate(response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets HTTP tracing On.Writes output to Console
|
|
/// </summary>
|
|
public void SetTraceOn(IRequestLogger logger = null)
|
|
{
|
|
this.logger = logger ?? new DefaultRequestLogger();
|
|
this.trace = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets HTTP tracing Off.
|
|
/// </summary>
|
|
public void SetTraceOff()
|
|
{
|
|
this.trace = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs the request sent to server and corresponding response
|
|
/// </summary>
|
|
/// <param name="request"></param>
|
|
/// <param name="response"></param>
|
|
/// <param name="durationMs"></param>
|
|
private void LogRequest(IRestRequest request, IRestResponse response, double durationMs)
|
|
{
|
|
var requestToLog = new RequestToLog
|
|
{
|
|
resource = request.Resource,
|
|
// Parameters are custom anonymous objects in order to have the parameter type as a nice string
|
|
// otherwise it will just show the enum value
|
|
parameters = request.Parameters.Select(parameter => new RequestParameter
|
|
{
|
|
name = parameter.Name,
|
|
value = parameter.Value,
|
|
type = parameter.Type.ToString()
|
|
}),
|
|
// ToString() here to have the method as a nice string otherwise it will just show the enum value
|
|
method = request.Method.ToString(),
|
|
// This will generate the actual Uri used in the request
|
|
uri = restClient.BuildUri(request)
|
|
};
|
|
|
|
var responseToLog = new ResponseToLog
|
|
{
|
|
statusCode = response.StatusCode,
|
|
content = response.Content,
|
|
headers = response.Headers,
|
|
// The Uri that actually responded (could be different from the requestUri if a redirection occurred)
|
|
responseUri = response.ResponseUri,
|
|
errorMessage = response.ErrorMessage,
|
|
durationMs = durationMs
|
|
};
|
|
|
|
this.logger.LogRequest(requestToLog, responseToLog, durationMs);
|
|
}
|
|
|
|
private Task<IRestResponse> ExecuteWithRetry(
|
|
Func<Task<IRestResponse>> executeRequestCallback)
|
|
{
|
|
return retryPolicyHandler == null
|
|
? executeRequestCallback()
|
|
: retryPolicyHandler(executeRequestCallback);
|
|
}
|
|
}
|
|
|
|
internal delegate void ApiResponseErrorHandlingDelegate(IRestResponse response);
|
|
|
|
public delegate Task<IRestResponse> RetryPolicyHandlingDelegate(
|
|
Func<Task<IRestResponse>> executeRequestCallback);
|
|
}
|