/*
* 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.Util.Internal;
using Amazon.Runtime.Internal.Util;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Xml;
using ThirdParty.Ionic.Zlib;
namespace Amazon.Runtime.Internal.Transform
{
///
/// Base class for the UnmarshallerContext objects that are used
/// to unmarshall a web-service response.
///
public abstract class UnmarshallerContext : IDisposable
{
private bool disposed = false;
protected bool MaintainResponseBody { get; set; }
protected bool IsException { get; set; }
protected CrcCalculatorStream CrcStream { get; set; }
protected int Crc32Result { get; set; }
protected CoreChecksumAlgorithm ChecksumAlgorithm { get; set; }
protected HashStream FlexibleChecksumStream { get; set; }
protected string ExpectedFlexibleChecksumResult { get; set; }
protected IWebResponseData WebResponseData { get; set; }
protected CachingWrapperStream WrappingStream { get; set; }
public string ResponseBody
{
get
{
var bytes = GetResponseBodyBytes();
return System.Text.UTF8Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
}
public byte[] GetResponseBodyBytes()
{
if (IsException)
{
return this.WrappingStream.AllReadBytes.ToArray();
}
if (MaintainResponseBody)
{
return this.WrappingStream.LoggableReadBytes.ToArray();
}
else
{
return ArrayEx.Empty();
}
}
public IWebResponseData ResponseData
{
get { return WebResponseData; }
}
internal void ValidateCRC32IfAvailable()
{
if (this.CrcStream != null)
{
if (this.CrcStream.Crc32 != this.Crc32Result)
{
throw new IOException("CRC value returned with response does not match the computed CRC value for the returned response body.");
}
}
}
internal void ValidateFlexibleCheckumsIfAvailable(ResponseMetadata responseMetadata)
{
if (FlexibleChecksumStream == null)
{
return;
}
responseMetadata.ChecksumAlgorithm = ChecksumAlgorithm;
responseMetadata.ChecksumValidationStatus = ChecksumValidationStatus.PENDING_RESPONSE_READ;
if (FlexibleChecksumStream.CalculatedHash != null)
{
if (Convert.ToBase64String(FlexibleChecksumStream.CalculatedHash) != ExpectedFlexibleChecksumResult)
{
responseMetadata.ChecksumValidationStatus = ChecksumValidationStatus.INVALID;
throw new AmazonClientException("Expected hash not equal to calculated hash");
}
else
{
responseMetadata.ChecksumValidationStatus = ChecksumValidationStatus.SUCCESSFUL;
}
}
}
protected void SetupCRCStream(IWebResponseData responseData, Stream responseStream, long contentLength)
{
this.CrcStream = null;
UInt32 parsed;
if (responseData != null && UInt32.TryParse(responseData.GetHeaderValue("x-amz-crc32"), out parsed))
{
this.Crc32Result = unchecked((int) parsed);
this.CrcStream = new CrcCalculatorStream(responseStream, contentLength);
}
}
protected void SetupFlexibleChecksumStream(IWebResponseData responseData, Stream responseStream, long contentLength, IRequestContext requestContext)
{
var algorithm = ChecksumUtils.SelectChecksumForResponseValidation(requestContext?.OriginalRequest?.ChecksumResponseAlgorithms, responseData);
if (algorithm == CoreChecksumAlgorithm.NONE)
{
return;
}
ChecksumAlgorithm = algorithm;
ExpectedFlexibleChecksumResult = responseData.GetHeaderValue(ChecksumUtils.GetChecksumHeaderKey(algorithm));
var checksum = Convert.FromBase64String(ExpectedFlexibleChecksumResult);
switch (algorithm)
{
case CoreChecksumAlgorithm.CRC32C:
FlexibleChecksumStream = new HashStream(responseStream, checksum, contentLength);
break;
case CoreChecksumAlgorithm.CRC32:
FlexibleChecksumStream = new HashStream(responseStream, checksum, contentLength);
break;
case CoreChecksumAlgorithm.SHA256:
FlexibleChecksumStream = new HashStream(responseStream, checksum, contentLength);
break;
case CoreChecksumAlgorithm.SHA1:
FlexibleChecksumStream = new HashStream(responseStream, checksum, contentLength);
break;
default:
throw new AmazonClientException($"Unsupported checksum algorithm {algorithm}");
}
}
///
/// Tests the specified expression against the current position in the XML
/// document
///
/// The pseudo-XPath expression to test.
///
/// True if the expression matches the current position in the document,
/// false otherwise.
public bool TestExpression(string expression)
{
return TestExpression(expression, CurrentPath);
}
///
/// Tests the specified expression against the current position in the XML
/// document being parsed, and restricts the expression to matching at the
/// specified stack depth.
///
/// The pseudo-XPath expression to test.
///
/// The depth in the stack representing where the expression must
/// start matching in order for this method to return true.
///
/// True if the specified expression matches the current position in
/// the XML document, starting from the specified depth.
public bool TestExpression(string expression, int startingStackDepth)
{
return TestExpression(expression, startingStackDepth, CurrentPath, CurrentDepth);
}
///
/// Reads the next token at depth greater than or equal to target depth.
///
/// Tokens are read at depth greater than or equal to target depth.
/// True if a token was read and current depth is greater than or equal to target depth.
public bool ReadAtDepth(int targetDepth)
{
return Read() && this.CurrentDepth >= targetDepth;
}
private static bool TestExpression(string expression, string currentPath)
{
if (expression.Equals("."))
return true;
return currentPath.EndsWith(expression, StringComparison.OrdinalIgnoreCase);
}
private static bool TestExpression(string expression, int startingStackDepth, string currentPath, int currentDepth)
{
if (expression.Equals("."))
return true;
int index = -1;
while ((index = expression.IndexOf("/", index + 1, StringComparison.Ordinal)) > -1)
{
// Don't consider attributes a new depth level
if (expression[0] != '@')
{
startingStackDepth++;
}
}
return startingStackDepth == currentDepth
&& currentPath.Length > expression.Length
&& currentPath[currentPath.Length - expression.Length - 1] == '/'
&& currentPath.EndsWith(expression, StringComparison.OrdinalIgnoreCase);
}
#region Abstract members
///
/// The current path that is being unmarshalled.
///
public abstract string CurrentPath { get; }
///
/// Returns the element depth of the parser's current position in the
/// document being parsed.
///
public abstract int CurrentDepth { get; }
///
/// Reads to the next node in the document, and updates the context accordingly.
///
///
/// True if a node was read, false if there are no more elements to read.
///
public abstract bool Read();
///
/// Returns the text contents of the current element being parsed.
///
///
/// The text contents of the current element being parsed.
///
public abstract string ReadText();
///
/// True if NodeType is Element.
///
public abstract bool IsStartElement { get; }
///
/// True if NodeType is EndElement.
///
public abstract bool IsEndElement { get; }
///
/// True if the context is at the start of the document.
///
public abstract bool IsStartOfDocument { get; }
#endregion
#region Dispose Pattern Implementation
///
/// Implements the Dispose pattern
///
/// Whether this object is being disposed via a call to Dispose
/// or garbage collected.
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
if (this.CrcStream != null)
{
CrcStream.Dispose();
CrcStream = null;
}
if (this.WrappingStream != null)
{
WrappingStream.Dispose();
WrappingStream = null;
}
}
this.disposed = true;
}
}
///
/// Disposes of all managed and unmanaged resources.
///
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
///
/// Wrap an XmltextReader for simulating an event stream.
///
/// Each Read() operation goes either to the next element or next attribute within
/// the current element. TestExpression() is used to match the current event
/// to an xpath expression. The general pattern looks like this:
///
/// UnmarshallerContext context = new UnmarshallerContext(...);
/// while (context.Read())
/// {
/// if (context.TestExpresion("path/to/element"))
/// {
/// myObject.stringMember = stringUnmarshaller.GetInstance().Unmarshall(context);
/// continue;
/// }
/// if (context.TestExpression("path/to/@attribute"))
/// myObject.MyComplexTypeMember = MyComplexTypeUnmarshaller.GetInstance().Unmarshall(context);
/// }
///
///
public class XmlUnmarshallerContext : UnmarshallerContext
{
#region Private members
private static HashSet nodesToSkip = new HashSet
{
XmlNodeType.None,
XmlNodeType.XmlDeclaration,
XmlNodeType.Comment,
XmlNodeType.DocumentType
};
private StreamReader streamReader;
private XmlTextReader _xmlTextReader;
private Stack stack = new Stack();
private string stackString = "";
private Dictionary attributeValues;
private List attributeNames;
private IEnumerator attributeEnumerator;
private XmlNodeType nodeType;
private string nodeContent = String.Empty;
private bool disposed = false;
private bool currentlyProcessingEmptyElement;
public Stream Stream
{
get
{
return streamReader.BaseStream;
}
}
///
/// Lookup of element names that are not skipped if empty within the XML response structure.
///
public HashSet AllowEmptyElementLookup { get; private set; }
///
/// Despite Microsoft's recommendation to use XmlReader for .NET Framework 2.0 or greater
/// (https://docs.microsoft.com/en-us/dotnet/api/system.xml.xmltextreader#remarks), this class
/// intentionally uses XmlTextReader to handle the XML related object key constraints
/// for S3 (https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html).
///
private XmlTextReader XmlReader
{
get
{
if (_xmlTextReader == null)
{
_xmlTextReader = new XmlTextReader(streamReader);
_xmlTextReader.WhitespaceHandling = WhitespaceHandling.None;
#if BCL35
_xmlTextReader.ProhibitDtd = false;
#else
_xmlTextReader.DtdProcessing = DtdProcessing.Ignore;
#endif
}
return _xmlTextReader;
}
}
#endregion
#region Constructors
///
/// Wrap an XmlTextReader with state for event-based parsing of an XML stream.
///
/// Stream with the XML from a service response.
/// If set to true, maintains a copy of the complete response body constraint to log response size as the stream is being read.
/// Response data coming back from the request
/// If set to true, maintains a copy of the complete response body as the stream is being read.
public XmlUnmarshallerContext(Stream responseStream, bool maintainResponseBody, IWebResponseData responseData, bool isException = false)
: this(responseStream, maintainResponseBody, responseData, isException, null)
{
}
///
/// Wrap an XmlTextReader with state for event-based parsing of an XML stream.
///
/// Stream with the XML from a service response.
/// If set to true, maintains a copy of the complete response body constraint to log response size as the stream is being read.
/// Response data coming back from the request
/// If set to true, maintains a copy of the complete response body as the stream is being read.
/// Context for the request that produced this response
public XmlUnmarshallerContext(Stream responseStream, bool maintainResponseBody, IWebResponseData responseData, bool isException, IRequestContext requestContext)
{
if (isException)
{
this.WrappingStream = new CachingWrapperStream(responseStream);
}
else if (maintainResponseBody)
{
this.WrappingStream = new CachingWrapperStream(responseStream, AWSConfigs.LoggingConfig.LogResponsesSizeLimit);
}
if (isException || maintainResponseBody)
{
responseStream = this.WrappingStream;
}
// If the unmarshaller context is being called internally without there being a http response then the response data would be null
if (responseData != null)
{
long contentLength;
bool parsedContentLengthHeader = long.TryParse(responseData.GetHeaderValue("Content-Length"), out contentLength);
// Validate flexible checksums if we know the content length and the behavior was opted in to on the request
if (parsedContentLengthHeader && responseData.ContentLength == contentLength &&
string.IsNullOrEmpty(responseData.GetHeaderValue("Content-Encoding")) &&
requestContext?.OriginalRequest?.CoreChecksumMode == CoreChecksumResponseBehavior.ENABLED)
{
SetupFlexibleChecksumStream(responseData, responseStream, contentLength, requestContext);
}
}
streamReader = new StreamReader(this.FlexibleChecksumStream ?? responseStream);
this.WebResponseData = responseData;
this.MaintainResponseBody = maintainResponseBody;
this.IsException = isException;
this.AllowEmptyElementLookup = new HashSet();
}
#endregion
#region Overrides
///
/// The current XML path that is being unmarshalled.
///
public override string CurrentPath
{
get { return this.stackString; }
}
///
/// Returns the element depth of the parser's current position in the XML
/// document being parsed.
///
public override int CurrentDepth
{
get { return stack.Count; }
}
///
/// Reads to the next node in the XML document, and updates the context accordingly.
///
///
/// True if a node was read, false if there are no more elements to read./
///
public override bool Read()
{
if (attributeEnumerator != null && attributeEnumerator.MoveNext())
{
this.nodeType = XmlNodeType.Attribute;
stackString = string.Format(CultureInfo.InvariantCulture, "{0}/@{1}", StackToPath(stack), attributeEnumerator.Current);
}
else
{
// Skip some nodes
if (nodesToSkip.Contains(XmlReader.NodeType))
XmlReader.Read();
while (XmlReader.IsEmptyElement && !AllowEmptyElementLookup.Contains(XmlReader.LocalName))
{
XmlReader.Read();
}
if (currentlyProcessingEmptyElement)
{
nodeType = XmlNodeType.EndElement;
stack.Pop();
stackString = StackToPath(stack);
XmlReader.Read();
currentlyProcessingEmptyElement = false;
}
else if(XmlReader.IsEmptyElement && AllowEmptyElementLookup.Contains(XmlReader.LocalName))
{
//This is a shorthand form of an empty element and we want to allow it
nodeType = XmlNodeType.Element;
stack.Push(XmlReader.LocalName);
stackString = StackToPath(stack);
currentlyProcessingEmptyElement = true;
//Defer reading so that on next pass we can treat this same element as the end element.
}
else
{
switch (XmlReader.NodeType)
{
case XmlNodeType.EndElement:
this.nodeType = XmlNodeType.EndElement;
stack.Pop();
stackString = StackToPath(stack);
XmlReader.Read();
break;
case XmlNodeType.Element:
nodeType = XmlNodeType.Element;
stack.Push(XmlReader.LocalName);
stackString = StackToPath(stack);
this.ReadElement();
break;
}
}
}
bool moreDataAvailable =
XmlReader.ReadState != ReadState.EndOfFile &&
XmlReader.ReadState != ReadState.Error &&
XmlReader.ReadState != ReadState.Closed;
return moreDataAvailable;
}
///
/// Returns the text contents of the current element being parsed.
///
///
/// The text contents of the current element being parsed.
///
public override string ReadText()
{
if (this.nodeType == XmlNodeType.Attribute)
{
return (attributeValues[attributeEnumerator.Current]);
}
else
{
return nodeContent;
}
}
///
/// True if NodeType is Element.
///
public override bool IsStartElement
{
get { return this.nodeType == XmlNodeType.Element; }
}
///
/// True if NodeType is EndElement.
///
public override bool IsEndElement
{
get { return this.nodeType == XmlNodeType.EndElement; }
}
///
/// True if the context is at the start of the document.
///
public override bool IsStartOfDocument
{
get { return XmlReader.ReadState == ReadState.Initial; }
}
#endregion
#region Public methods
///
/// True if NodeType is Attribute.
///
public bool IsAttribute
{
get { return this.nodeType == XmlNodeType.Attribute; }
}
#endregion
#region Private Methods
private static string StackToPath(Stack stack)
{
string path = null;
foreach (string s in stack.ToArray())
{
path = null == path ? s : string.Format(CultureInfo.InvariantCulture, "{0}/{1}", s, path);
}
return "/" + path;
}
// Move to the next element, cache the attributes collection
// and attempt to cache the inner text of the element if applicable.
private void ReadElement()
{
if (XmlReader.HasAttributes)
{
attributeValues = new Dictionary();
attributeNames = new List();
while (XmlReader.MoveToNextAttribute())
{
attributeValues.Add(XmlReader.LocalName, XmlReader.Value);
attributeNames.Add(XmlReader.LocalName);
}
attributeEnumerator = attributeNames.GetEnumerator();
}
XmlReader.MoveToElement();
XmlReader.Read();
if (XmlReader.NodeType == XmlNodeType.Text)
nodeContent = XmlReader.ReadContentAsString();
else
nodeContent = String.Empty;
}
#endregion
protected override void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
if (streamReader != null)
{
streamReader.Dispose();
streamReader = null;
}
if (_xmlTextReader != null)
{
#if NETSTANDARD
_xmlTextReader.Dispose();
#else
_xmlTextReader.Close();
#endif
_xmlTextReader = null;
}
}
disposed = true;
}
base.Dispose(disposing);
}
}
public class EC2UnmarshallerContext : XmlUnmarshallerContext
{
///
/// Wrap an XmlTextReader with state for event-based parsing of an XML stream.
///
/// Stream with the XML from a service response.
/// If set to true, maintains a copy of the complete response body constraint to log response size as the stream is being read.
/// Response data coming back from the request
/// If set to true, maintains a copy of the complete response body as the stream is being read.
public EC2UnmarshallerContext(Stream responseStream, bool maintainResponseBody, IWebResponseData responseData, bool isException = false)
: base(responseStream, maintainResponseBody, responseData, isException)
{
}
///
/// Wrap an XmlTextReader with state for event-based parsing of an XML stream.
///
/// Stream with the XML from a service response.
/// If set to true, maintains a copy of the complete response body constraint to log response size as the stream is being read.
/// Response data coming back from the request
/// If set to true, maintains a copy of the complete response body as the stream is being read.
/// Context for the request that produced this response
public EC2UnmarshallerContext(Stream responseStream, bool maintainResponseBody, IWebResponseData responseData, bool isException, IRequestContext requestContext)
: base(responseStream, maintainResponseBody, responseData, isException, requestContext)
{
}
///
/// RequestId value, if found in response
///
public string RequestId { get; private set; }
///
/// Reads to the next node in the XML document, and updates the context accordingly.
/// If node is RequestId, reads the contents and stores in RequestId property.
///
///
/// True if a node was read, false if there are no more elements to read./
///
public override bool Read()
{
bool result = base.Read();
if (RequestId == null)
{
if (IsStartElement && TestExpression("RequestId", 2))
{
RequestId = StringUnmarshaller.GetInstance().Unmarshall(this);
}
}
return result;
}
}
}