/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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 Amazon.Runtime.Internal; using Amazon.Runtime.Internal.Auth; using Amazon.Runtime.Internal.Transform; using Amazon.Runtime.Internal.Util; using Amazon.Util; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using System.Text; using System.Linq; using System.Threading; using Amazon.Util.Internal; using ExecutionContext = Amazon.Runtime.Internal.ExecutionContext; namespace Amazon.Runtime { public abstract class AmazonServiceClient : IDisposable { private static volatile bool _isProtocolUpdated; private bool _disposed; private Logger _logger; protected EndpointDiscoveryResolverBase EndpointDiscoveryResolver { get; private set; } protected RuntimePipeline RuntimePipeline { get; set; } protected internal AWSCredentials Credentials { get; private set; } public IClientConfig Config => _config; private readonly ClientConfig _config; protected virtual IServiceMetadata ServiceMetadata { get; } = new ServiceMetadata(); protected virtual bool SupportResponseLogging { get { return true; } } #region Events private PreRequestEventHandler mBeforeMarshallingEvent; /// /// Occurs before a request is marshalled. /// internal event PreRequestEventHandler BeforeMarshallingEvent { add { lock (this) { mBeforeMarshallingEvent += value; } } remove { lock (this) { mBeforeMarshallingEvent -= value; } } } private RequestEventHandler mBeforeRequestEvent; /// /// Occurs before a request is issued against the service. /// public event RequestEventHandler BeforeRequestEvent { add { lock (this) { mBeforeRequestEvent += value; } } remove { lock (this) { mBeforeRequestEvent -= value; } } } private ResponseEventHandler mAfterResponseEvent; /// /// Occurs after a response is received from the service. /// public event ResponseEventHandler AfterResponseEvent { add { lock (this) { mAfterResponseEvent += value; } } remove { lock (this) { mAfterResponseEvent -= value; } } } private ExceptionEventHandler mExceptionEvent; /// /// Occurs after an exception is encountered. /// public event ExceptionEventHandler ExceptionEvent { add { lock (this) { mExceptionEvent += value; } } remove { lock (this) { mExceptionEvent -= value; } } } #endregion #region Constructors protected AmazonServiceClient(AWSCredentials credentials, ClientConfig config) { if (config.DisableLogging) _logger = Logger.EmptyLogger; else _logger = Logger.GetLogger(this.GetType()); config.Validate(); this.Credentials = credentials; _config = config; Signer = CreateSigner(); EndpointDiscoveryResolver = new EndpointDiscoveryResolver(config, _logger); Initialize(); UpdateSecurityProtocol(); BuildRuntimePipeline(); } protected AbstractAWSSigner Signer { get; private set; } protected AmazonServiceClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, ClientConfig config) : this(new SessionAWSCredentials(awsAccessKeyId, awsSecretAccessKey, awsSessionToken), config) { } protected AmazonServiceClient(string awsAccessKeyId, string awsSecretAccessKey, ClientConfig config) : this(new BasicAWSCredentials(awsAccessKeyId, awsSecretAccessKey), config) { } protected virtual void Initialize() { } #endregion #region Invoke methods [Obsolete("Invoke taking marshallers is obsolete. Use Invoke taking InvokeOptionsBase instead.")] protected TResponse Invoke(TRequest request, IMarshaller marshaller, ResponseUnmarshaller unmarshaller) where TRequest : AmazonWebServiceRequest where TResponse : AmazonWebServiceResponse { var options = new InvokeOptions(); options.RequestMarshaller = marshaller; options.ResponseUnmarshaller = unmarshaller; return Invoke(request, options); } protected TResponse Invoke(AmazonWebServiceRequest request, InvokeOptionsBase options) where TResponse : AmazonWebServiceResponse { ThrowIfDisposed(); var executionContext = new ExecutionContext( new RequestContext(this.Config.LogMetrics, Signer) { ClientConfig = this.Config, Marshaller = options.RequestMarshaller, OriginalRequest = request, Unmarshaller = options.ResponseUnmarshaller, IsAsync = false, ServiceMetaData = this.ServiceMetadata, Options = options }, new ResponseContext() ); SetupCSMHandler(executionContext.RequestContext); var response = (TResponse)this.RuntimePipeline.InvokeSync(executionContext).Response; return response; } #if AWS_ASYNC_API [Obsolete("InvokeAsync taking marshallers is obsolete. Use InvokeAsync taking InvokeOptionsBase instead.")] protected System.Threading.Tasks.Task InvokeAsync( TRequest request, IMarshaller marshaller, ResponseUnmarshaller unmarshaller, System.Threading.CancellationToken cancellationToken) where TRequest: AmazonWebServiceRequest where TResponse : AmazonWebServiceResponse, new() { var options = new InvokeOptions(); options.RequestMarshaller = marshaller; options.ResponseUnmarshaller = unmarshaller; return InvokeAsync(request, options, cancellationToken); } protected System.Threading.Tasks.Task InvokeAsync( AmazonWebServiceRequest request, InvokeOptionsBase options, System.Threading.CancellationToken cancellationToken) where TResponse : AmazonWebServiceResponse, new() { ThrowIfDisposed(); #if AWS_ASYNC_API if (cancellationToken == default(CancellationToken)) cancellationToken = _config.BuildDefaultCancellationToken(); #endif var executionContext = new ExecutionContext( new RequestContext(this.Config.LogMetrics, Signer) { ClientConfig = this.Config, Marshaller = options.RequestMarshaller, OriginalRequest = request, Unmarshaller = options.ResponseUnmarshaller, IsAsync = true, CancellationToken = cancellationToken, ServiceMetaData = this.ServiceMetadata, Options = options }, new ResponseContext() ); SetupCSMHandler(executionContext.RequestContext); return this.RuntimePipeline.InvokeAsync(executionContext); } #elif AWS_APM_API [Obsolete("BeginInvoke taking marshallers is obsolete. Use BeginInvoke taking InvokeOptionsBase instead.")] protected IAsyncResult BeginInvoke(TRequest request, IMarshaller marshaller, ResponseUnmarshaller unmarshaller, AsyncCallback callback, object state) where TRequest : AmazonWebServiceRequest { var options = new InvokeOptions(); options.RequestMarshaller = marshaller; options.ResponseUnmarshaller = unmarshaller; return BeginInvoke(request, options, callback, state); } protected IAsyncResult BeginInvoke(AmazonWebServiceRequest request, InvokeOptionsBase options, AsyncCallback callback, object state) { ThrowIfDisposed(); var executionContext = new AsyncExecutionContext( new AsyncRequestContext(this.Config.LogMetrics, Signer) { ClientConfig = this.Config, Marshaller = options.RequestMarshaller, OriginalRequest = request, Unmarshaller = options.ResponseUnmarshaller, Callback = callback, State = state, IsAsync = true, ServiceMetaData = this.ServiceMetadata, Options = options }, new AsyncResponseContext() ); SetupCSMHandler(executionContext.RequestContext); var asyncResult = this.RuntimePipeline.InvokeAsync(executionContext); return asyncResult; } protected static TResponse EndInvoke(IAsyncResult result) where TResponse : AmazonWebServiceResponse { if (result == null) throw new ArgumentNullException("result", "Parameter result cannot be null."); var asyncResult = result as RuntimeAsyncResult; if (asyncResult == null) throw new ArgumentOutOfRangeException("result", "Parameter result is not of type RuntimeAsyncResult."); using (asyncResult) { if (!asyncResult.IsCompleted) { asyncResult.AsyncWaitHandle.WaitOne(); } if (asyncResult.Exception != null) { AWSSDKUtils.PreserveStackTrace(asyncResult.Exception); throw asyncResult.Exception; } return (TResponse)asyncResult.Response; } } #endif protected virtual IEnumerable EndpointOperation(EndpointOperationContextBase context) { return null; } #endregion #region Process Handlers protected void ProcessPreRequestHandlers(IExecutionContext executionContext) { //if (request == null) // return; if (mBeforeMarshallingEvent == null) return; PreRequestEventArgs args = PreRequestEventArgs.Create(executionContext.RequestContext.OriginalRequest); mBeforeMarshallingEvent(this, args); } protected void ProcessRequestHandlers(IExecutionContext executionContext) { var request = executionContext.RequestContext.Request; WebServiceRequestEventArgs args = WebServiceRequestEventArgs.Create(request); if (request.OriginalRequest != null) request.OriginalRequest.FireBeforeRequestEvent(this, args); if (mBeforeRequestEvent != null) mBeforeRequestEvent(this, args); } protected void ProcessResponseHandlers(IExecutionContext executionContext) { if (mAfterResponseEvent == null) return; WebServiceResponseEventArgs args = WebServiceResponseEventArgs.Create( executionContext.ResponseContext.Response, executionContext.RequestContext.Request, executionContext.ResponseContext.HttpResponse); mAfterResponseEvent(this, args); } protected virtual void ProcessExceptionHandlers(IExecutionContext executionContext, Exception exception) { if (mExceptionEvent == null) return; WebServiceExceptionEventArgs args = WebServiceExceptionEventArgs.Create(exception, executionContext.RequestContext.Request); mExceptionEvent(this, args); } #endregion #region Dispose methods public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (RuntimePipeline != null) RuntimePipeline.Dispose(); _disposed = true; } } private void ThrowIfDisposed() { if (this._disposed) throw new ObjectDisposedException(GetType().FullName); } #endregion protected abstract AbstractAWSSigner CreateSigner(); protected virtual void CustomizeRuntimePipeline(RuntimePipeline pipeline) { } private void BuildRuntimePipeline() { #if BCL var httpRequestFactory = new HttpWebRequestFactory(); var httpHandler = new HttpHandler(httpRequestFactory, this); #else var httpRequestFactory = new HttpRequestMessageFactory(this.Config); var httpHandler = new HttpHandler(httpRequestFactory, this); #endif var preMarshallHandler = new CallbackHandler(); preMarshallHandler.OnPreInvoke = this.ProcessPreRequestHandlers; var postMarshallHandler = new CallbackHandler(); postMarshallHandler.OnPreInvoke = this.ProcessRequestHandlers; var postUnmarshallHandler = new CallbackHandler(); postUnmarshallHandler.OnPostInvoke = this.ProcessResponseHandlers; var errorCallbackHandler = new ErrorCallbackHandler(); errorCallbackHandler.OnError = this.ProcessExceptionHandlers; //Determine which retry policy to use based on the retry mode RetryPolicy retryPolicy; switch (this.Config.RetryMode) { case RequestRetryMode.Adaptive: retryPolicy = new AdaptiveRetryPolicy(this.Config); break; case RequestRetryMode.Standard: retryPolicy = new StandardRetryPolicy(this.Config); break; case RequestRetryMode.Legacy: retryPolicy = new DefaultRetryPolicy(this.Config); break; default: throw new InvalidOperationException("Unknown retry mode"); } // Build default runtime pipeline. this.RuntimePipeline = new RuntimePipeline(new List { httpHandler, new Unmarshaller(this.SupportResponseLogging), new ErrorHandler(_logger), postUnmarshallHandler, new Signer(), //EndpointDiscoveryResolver must come after CredentialsRetriever, RetryHander, and EndpointResolver as it depends on //credentials, retrying of requests for 421 web exceptions, and the current set regional endpoint. new EndpointDiscoveryHandler(), new CredentialsRetriever(this.Credentials), new RetryHandler(retryPolicy), postMarshallHandler, new EndpointResolver(), new Marshaller(), preMarshallHandler, errorCallbackHandler, new MetricsHandler() }, _logger ); if (DeterminedCSMConfiguration.Instance.CSMConfiguration.Enabled && !string.IsNullOrEmpty(ServiceMetadata.ServiceId)) { this.RuntimePipeline.AddHandlerBefore(new CSMCallAttemptHandler()); this.RuntimePipeline.AddHandlerBefore(new CSMCallEventHandler()); } CustomizeRuntimePipeline(this.RuntimePipeline); // Apply global pipeline customizations RuntimePipelineCustomizerRegistry.Instance.ApplyCustomizations(this.GetType(), this.RuntimePipeline); } /// /// Some AWS services like Cloud 9 require at least TLS 1.1. Version of .NET Framework 4.5 and earlier /// do not eanble TLS 1.1 and TLS 1.2 by default. This code adds those protocols if using an earlier /// version of .NET that explicitly set the protocol and didn't have TLS 1.1 and TLS 1.2. /// private void UpdateSecurityProtocol() { if (_isProtocolUpdated) return; var amazonSecurityProtocolManager = new AmazonSecurityProtocolManager(); try { if (!amazonSecurityProtocolManager.IsSecurityProtocolSystemDefault()) { amazonSecurityProtocolManager.UpdateProtocolsToSupported(); } } catch (Exception ex) { if (ex is NotSupportedException) { _logger.InfoFormat(ex.Message); } else { _logger.InfoFormat("Unexpected error " + ex.GetType().Name + " encountered when trying to set Security Protocol.\n" + ex); } } _isProtocolUpdated = true; } /// /// Assembles the Uri for a given SDK request /// /// Request to compute Uri for /// Uri for the given SDK request public static Uri ComposeUrl(IRequest iRequest) { return ComposeUrl(iRequest, true); } /// /// Assembles the Uri for a given SDK request /// /// Request to compute Uri for /// If true the accepted path characters {/+:} are not encoded. /// Uri for the given SDK request public static Uri ComposeUrl(IRequest internalRequest, bool skipEncodingValidPathChars) { Uri url = internalRequest.Endpoint; var resourcePath = internalRequest.ResourcePath; if (resourcePath == null) resourcePath = string.Empty; else { if (resourcePath.StartsWith("/", StringComparison.Ordinal)) resourcePath = resourcePath.Substring(1); // Microsoft added support for unicode bidi control characters to the Uri class in .NET 4.7.2 // https://github.com/microsoft/dotnet/blob/master/Documentation/compatibility/uri-unicode-bidirectional-characters.md // However, we only want to support it on .NET Core 3.1 and higher due to not having to deal with .NET Standard support matrix. #if BCL || NETSTANDARD20 if (AWSSDKUtils.HasBidiControlCharacters(resourcePath) || (internalRequest.PathResources?.Any(v => AWSSDKUtils.HasBidiControlCharacters(v.Value)) == true)) { resourcePath = string.Join("/", AWSSDKUtils.SplitResourcePathIntoSegments(resourcePath, internalRequest.PathResources).ToArray()); throw new AmazonClientException(string.Format(CultureInfo.InvariantCulture, "Target resource path [{0}] has bidirectional characters, which are not supported" + "by System.Uri and thus cannot be handled by the .NET SDK.", resourcePath)); } #endif resourcePath = AWSSDKUtils.ResolveResourcePath(resourcePath, internalRequest.PathResources, skipEncodingValidPathChars); } // Construct any sub resource/query parameter additions to append to the // resource path. Services like S3 which allow '?' and/or '&' in resource paths // should use SubResources instead of appending them to the resource path with // query string delimiters during request marshalling. var delim = "?"; var sb = new StringBuilder(); if (internalRequest.SubResources?.Count > 0) { foreach (var subResource in internalRequest.SubResources) { sb.AppendFormat("{0}{1}", delim, subResource.Key); if (subResource.Value != null) sb.AppendFormat("={0}", subResource.Value); delim = "&"; } } if (internalRequest.UseQueryString && internalRequest.Parameters?.Count > 0) { var queryString = AWSSDKUtils.GetParametersAsString(internalRequest); sb.AppendFormat("{0}{1}", delim, queryString); } var parameterizedPath = string.Empty; if(internalRequest.MarshallerVersion >= 2) { parameterizedPath = string.Concat(resourcePath, sb); } else { if (AWSSDKUtils.HasBidiControlCharacters(resourcePath)) throw new AmazonClientException(string.Format(CultureInfo.InvariantCulture, "Target resource path [{0}] has bidirectional characters, which are not supported" + "by System.Uri and thus cannot be handled by the .NET SDK.", resourcePath)); parameterizedPath = string.Concat(AWSSDKUtils.ProtectEncodedSlashUrlEncode(resourcePath, skipEncodingValidPathChars), sb); } var hasSlash = url.AbsoluteUri.EndsWith("/", StringComparison.Ordinal) || parameterizedPath.StartsWith("/", StringComparison.Ordinal); var uri = hasSlash ? new Uri(url.AbsoluteUri + parameterizedPath) : new Uri(url.AbsoluteUri + "/" + parameterizedPath); DontUnescapePathDotsAndSlashes(uri); return uri; } /// /// Patches the in-flight uri to stop it unescaping the path etc (what Uri did before /// Microsoft deprecated the constructor flag). This is particularly important for /// Amazon S3 customers who want to use backslash (\) in their key names. /// /// /// Different behavior in the various runtimes has been observed and in addition some /// 'documented' ways of doing this between 2.x and 4.x runtimes has also been observed /// to not be reliable. /// /// This patch effectively emulates what adding a schemesettings element to the /// app.config file with value 'name="http" genericUriParserOptions="DontUnescapePathDotsAndSlashes"' /// does. As we're a dll, that avenue is not open to us. /// /// private static void DontUnescapePathDotsAndSlashes(Uri uri) { #if BCL // System.UriSyntaxFlags is internal const int UnEscapeDotsAndSlashes = 0x2000000; if (uri == null) throw new ArgumentNullException("uri"); try { // currently prefer silent return than exceptions or log messages if reflection fails to // find the fields we need, otherwise we could generate a lot of noise if someone // runs on a platform without these fields FieldInfo fieldInfo = uri.GetType().GetField("m_Syntax", BindingFlags.Instance | BindingFlags.NonPublic); if (fieldInfo == null) return; var uriParser = fieldInfo.GetValue(uri); fieldInfo = typeof(UriParser).GetField("m_Flags", BindingFlags.Instance | BindingFlags.NonPublic); if (fieldInfo == null) return; var uriSyntaxFlags = fieldInfo.GetValue(uriParser); uriSyntaxFlags = (int)uriSyntaxFlags & ~UnEscapeDotsAndSlashes; fieldInfo.SetValue(uriParser, uriSyntaxFlags); } catch (Exception) { // swallow the exception because this platform doesn't support the hack to fix the big in the Uri class. } #endif } /// /// Used to create a copy of the config for a different service than the current instance. /// /// Target service ClientConfig /// The new ClientConfig for the desired service internal C CloneConfig() where C : ClientConfig, new() { var config = new C(); CloneConfig(config); return config; } internal void CloneConfig(ClientConfig newConfig) { if (!string.IsNullOrEmpty(this.Config.ServiceURL)) { var regionName = Util.AWSSDKUtils.DetermineRegion(this.Config.ServiceURL); RegionEndpoint region = RegionEndpoint.GetBySystemName(regionName); newConfig.RegionEndpoint = region; } else { newConfig.RegionEndpoint = this.Config.RegionEndpoint; } newConfig.UseHttp = this.Config.UseHttp; newConfig.ProxyCredentials = this.Config.ProxyCredentials; newConfig.ProxyHost = this.Config.ProxyHost; newConfig.ProxyPort = this.Config.ProxyPort; } private static void SetupCSMHandler(IRequestContext requestContext) { if (requestContext.CSMEnabled) { requestContext.CSMCallEvent = new MonitoringAPICallEvent(requestContext); } } } }