/* * 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; } } }