/*
* 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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Amazon.Runtime;
using Amazon.S3.Model;
using System.IO;
using Amazon.S3.Util;
using Amazon.Util;
using Amazon.Runtime.Internal.Util;
using Amazon.Runtime.Internal.Transform;
using Amazon.Runtime.Internal;
#pragma warning disable 1591
namespace Amazon.S3.Internal
{
public class AmazonS3ResponseHandler : PipelineHandler
{
///
/// Calls the post invoke logic after calling the next handler
/// in the pipeline.
///
/// The execution context which contains both the
/// requests and response context.
public override void InvokeSync(IExecutionContext executionContext)
{
base.InvokeSync(executionContext);
PostInvoke(executionContext);
}
#if AWS_ASYNC_API
///
/// Calls the and post invoke logic after calling the next handler
/// in the pipeline.
///
/// The response type for the current request.
/// The execution context, it contains the
/// request and response context.
/// A task that represents the asynchronous operation.
public override async System.Threading.Tasks.Task InvokeAsync(IExecutionContext executionContext)
{
var response = await base.InvokeAsync(executionContext).ConfigureAwait(false);
PostInvoke(executionContext);
return response;
}
#elif AWS_APM_API
///
/// Calls the PostInvoke methods after calling the next handler
/// in the pipeline.
///
/// The execution context, it contains the
/// request and response context.
protected override void InvokeAsyncCallback(IAsyncExecutionContext executionContext)
{
// Process the response if an exception hasn't occured
if (executionContext.ResponseContext.AsyncResult.Exception == null)
{
PostInvoke(ExecutionContext.CreateFromAsyncContext(executionContext));
}
base.InvokeAsyncCallback(executionContext);
}
#endif
protected virtual void PostInvoke(IExecutionContext executionContext)
{
ProcessResponseHandlers(executionContext);
}
private static void ProcessResponseHandlers(IExecutionContext executionContext)
{
AmazonWebServiceResponse response = executionContext.ResponseContext.Response;
IRequest request = executionContext.RequestContext.Request;
IWebResponseData webResponseData = executionContext.ResponseContext.HttpResponse;
bool isSse = HasSSEHeaders(webResponseData);
var getObjectResponse = response as GetObjectResponse;
if (getObjectResponse != null)
{
GetObjectRequest getObjectRequest = request.OriginalRequest as GetObjectRequest;
getObjectResponse.BucketName = getObjectRequest.BucketName;
getObjectResponse.Key = getObjectRequest.Key;
// If ETag is present and is an MD5 hash (not a multi-part upload ETag), and no byte range is specified,
// wrap the response stream in an MD5Stream.
// If there is a customer encryption algorithm the etag is not an MD5.
if (!string.IsNullOrEmpty(getObjectResponse.ETag)
&& !getObjectResponse.ETag.Contains("-")
&& !isSse
&& getObjectRequest.ByteRange == null)
{
string etag = getObjectResponse.ETag.Trim(etagTrimChars);
byte[] expectedHash = AWSSDKUtils.HexStringToBytes(etag);
HashStream hashStream = new MD5Stream(getObjectResponse.ResponseStream, expectedHash, getObjectResponse.ContentLength);
getObjectResponse.ResponseStream = hashStream;
}
}
var deleteObjectsResponse = response as DeleteObjectsResponse;
if (deleteObjectsResponse != null)
{
if (deleteObjectsResponse.DeleteErrors != null && deleteObjectsResponse.DeleteErrors.Count > 0)
{
throw new DeleteObjectsException(deleteObjectsResponse as DeleteObjectsResponse);
}
}
var putObjectResponse = response as PutObjectResponse;
var putObjectRequest = request.OriginalRequest as PutObjectRequest;
if (putObjectRequest != null)
{
// If InputStream was a MD5Stream, compare calculated hash to returned etag
MD5Stream hashStream = putObjectRequest.InputStream as MD5Stream;
if (hashStream != null)
{
if (putObjectResponse != null && !isSse)
{
// Stream may not have been closed, so force calculation of hash
hashStream.CalculateHash();
CompareHashes(putObjectResponse.ETag, hashStream.CalculatedHash);
}
// Set InputStream to its original value
putObjectRequest.InputStream = hashStream.GetNonWrapperBaseStream();
}
}
var listObjectsResponse = response as ListObjectsResponse;
if (listObjectsResponse != null)
{
if (listObjectsResponse.IsTruncated &&
string.IsNullOrEmpty(listObjectsResponse.NextMarker) &&
listObjectsResponse.S3Objects.Count > 0)
{
listObjectsResponse.NextMarker = listObjectsResponse.S3Objects.Last().Key;
}
}
var uploadPartRequest = request.OriginalRequest as UploadPartRequest;
var uploadPartResponse = response as UploadPartResponse;
if (uploadPartRequest != null)
{
if (uploadPartResponse != null)
uploadPartResponse.PartNumber = uploadPartRequest.PartNumber;
// If InputStream was a MD5Stream, compare calculated hash to returned etag
MD5Stream hashStream = uploadPartRequest.InputStream as MD5Stream;
if (hashStream != null)
{
if (uploadPartResponse != null && !isSse)
{
// Stream may not have been closed, so force calculation of hash
hashStream.CalculateHash();
CompareHashes(uploadPartResponse.ETag, hashStream.CalculatedHash);
}
// Set InputStream to its original value
uploadPartRequest.InputStream = hashStream.GetNonWrapperBaseStream();
}
}
var copyPartResponse = response as CopyPartResponse;
if (copyPartResponse != null)
{
copyPartResponse.PartNumber = ((CopyPartRequest)request.OriginalRequest).PartNumber;
}
AmazonS3Client.CleanupRequest(request.OriginalRequest);
}
private static bool HasSSEHeaders(IWebResponseData webResponseData)
{
bool usesCustomerAlgorithm = !string.IsNullOrEmpty(webResponseData.GetHeaderValue(HeaderKeys.XAmzSSECustomerAlgorithmHeader));
bool usesKmsKeyId = !string.IsNullOrEmpty(webResponseData.GetHeaderValue(HeaderKeys.XAmzServerSideEncryptionAwsKmsKeyIdHeader));
return usesCustomerAlgorithm || usesKmsKeyId;
}
private static char[] etagTrimChars = new char[] { '\"' };
// Compares ETag from S3 to calculated hash
// If ETag is empty or is for a multi-part upload, no comparison is made
// If ETag doesn't match the hash, an exception is thrown
private static void CompareHashes(string etag, byte[] hash)
{
if (string.IsNullOrEmpty(etag) || hash == null || hash.Length == 0)
return;
// if etag contains '-' character, the file was a multi-upload and we can't
// compare the etag to the hash value
if (etag.Contains("-"))
return;
etag = etag.Trim(etagTrimChars);
string hexHash = AWSSDKUtils.BytesToHexString(hash);
if (!string.Equals(etag, hexHash, StringComparison.OrdinalIgnoreCase))
throw new AmazonClientException("Expected hash not equal to calculated hash");
}
}
}