| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 | using System;using System.Collections.Generic;using System.Linq;using System.Net.Http;using System.Text;using System.IO;namespace Rssdp.Infrastructure{    /// <summary>    /// A base class for the <see cref="HttpResponseParser"/> and <see cref="HttpRequestParser"/> classes. Not intended for direct use.    /// </summary>    /// <typeparam name="T"></typeparam>    public abstract class HttpParserBase<T> where T : new()    {        #region Fields        private readonly string[] LineTerminators = new string[] { "\r\n", "\n" };        private readonly char[] SeparatorCharacters = new char[] { ',', ';' };        #endregion        #region Public Methods        /// <summary>        /// Parses the <paramref name="data"/> provided into either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.        /// </summary>        /// <param name="data">A string containing the HTTP message to parse.</param>        /// <returns>Either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object containing the parsed data.</returns>        public abstract T Parse(string data);        /// <summary>        /// Parses a string containing either an HTTP request or response into a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.        /// </summary>        /// <param name="message">A <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object representing the parsed message.</param>        /// <param name="headers">A reference to the <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the <paramref name="message"/> object.</param>        /// <param name="data">A string containing the data to be parsed.</param>        /// <returns>An <see cref="HttpContent"/> object containing the content of the parsed message.</returns>        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Honestly, it's fine. MemoryStream doesn't mind.")]        protected virtual void Parse(T message, System.Net.Http.Headers.HttpHeaders headers, string data)        {            if (data == null) throw new ArgumentNullException(nameof(data));            if (data.Length == 0) throw new ArgumentException("data cannot be an empty string.", nameof(data));            if (!LineTerminators.Any(data.Contains)) throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", nameof(data));            using (var retVal = new ByteArrayContent(Array.Empty<byte>()))            {                var lines = data.Split(LineTerminators, StringSplitOptions.None);                //First line is the 'request' line containing http protocol details like method, uri, http version etc.                ParseStatusLine(lines[0], message);                ParseHeaders(headers, retVal.Headers, lines);            }        }        /// <summary>        /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>.        /// </summary>        /// <param name="data">The first line of the HTTP message to be parsed.</param>        /// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param>        protected abstract void ParseStatusLine(string data, T message);        /// <summary>        /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false).        /// </summary>        /// <param name="headerName">A string containing the name of the header to return the type of.</param>        protected abstract bool IsContentHeader(string headerName);        /// <summary>        /// Parses the HTTP version text from an HTTP request or response status line and returns a <see cref="Version"/> object representing the parsed values.        /// </summary>        /// <param name="versionData">A string containing the HTTP version, from the message status line.</param>        /// <returns>A <see cref="Version"/> object containing the parsed version data.</returns>        protected Version ParseHttpVersion(string versionData)        {            if (versionData == null) throw new ArgumentNullException(nameof(versionData));            var versionSeparatorIndex = versionData.IndexOf('/');            if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length) throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", nameof(versionData));            return Version.Parse(versionData.Substring(versionSeparatorIndex + 1));        }        #endregion        #region Private Methods        /// <summary>        /// Parses a line from an HTTP request or response message containing a header name and value pair.        /// </summary>        /// <param name="line">A string containing the data to be parsed.</param>        /// <param name="headers">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection to which the parsed header will be added.</param>        /// <param name="contentHeaders">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the message content, to which the parsed header will be added.</param>        private void ParseHeader(string line, System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders)        {            //Header format is            //name: value            var headerKeySeparatorIndex = line.IndexOf(":", StringComparison.OrdinalIgnoreCase);            var headerName = line.Substring(0, headerKeySeparatorIndex).Trim();            var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim();            //Not sure how to determine where request headers and and content headers begin,            //at least not without a known set of headers (general headers first the content headers)            //which seems like a bad way of doing it. So we'll assume if it's a known content header put it there            //else use request headers.            var values = ParseValues(headerValue);            var headersToAddTo = IsContentHeader(headerName) ? contentHeaders : headers;            if (values.Count > 1)                headersToAddTo.TryAddWithoutValidation(headerName, values);            else                headersToAddTo.TryAddWithoutValidation(headerName, values.First());        }        private int ParseHeaders(System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders, string[] lines)        {            //Blank line separates headers from content, so read headers until we find blank line.            int lineIndex = 1;            string line = null, nextLine = null;            while (lineIndex + 1 < lines.Length && !String.IsNullOrEmpty((line = lines[lineIndex++])))            {                //If the following line starts with space or tab (or any whitespace), it is really part of this header but split for human readability.                //Combine these lines into a single comma separated style header for easier parsing.                while (lineIndex < lines.Length && !String.IsNullOrEmpty((nextLine = lines[lineIndex])))                {                    if (nextLine.Length > 0 && Char.IsWhiteSpace(nextLine[0]))                    {                        line += "," + nextLine.TrimStart();                        lineIndex++;                    }                    else                        break;                }                ParseHeader(line, headers, contentHeaders);            }            return lineIndex;        }        private IList<string> ParseValues(string headerValue)        {            // This really should be better and match the HTTP 1.1 spec,            // but this should actually be good enough for SSDP implementations            // I think.            var values = new List<string>();            if (headerValue == "\"\"")            {                values.Add(String.Empty);                return values;            }            var indexOfSeparator = headerValue.IndexOfAny(SeparatorCharacters);            if (indexOfSeparator <= 0)                values.Add(headerValue);            else            {                var segments = headerValue.Split(SeparatorCharacters);                if (headerValue.Contains("\""))                {                    for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++)                    {                        var segment = segments[segmentIndex];                        if (segment.Trim().StartsWith("\"", StringComparison.OrdinalIgnoreCase))                            segment = CombineQuotedSegments(segments, ref segmentIndex, segment);                        values.Add(segment);                    }                }                else                    values.AddRange(segments);            }            return values;        }        private string CombineQuotedSegments(string[] segments, ref int segmentIndex, string segment)        {            var trimmedSegment = segment.Trim();            for (int index = segmentIndex; index < segments.Length; index++)            {                if (trimmedSegment == "\"\"" ||                    (                        trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase)                        && !trimmedSegment.EndsWith("\"\"", StringComparison.OrdinalIgnoreCase)                        && !trimmedSegment.EndsWith("\\\"", StringComparison.OrdinalIgnoreCase))                    )                {                    segmentIndex = index;                    return trimmedSegment.Substring(1, trimmedSegment.Length - 2);                }                if (index + 1 < segments.Length)                    trimmedSegment += "," + segments[index + 1].TrimEnd();            }            segmentIndex = segments.Length;            if (trimmedSegment.StartsWith("\"", StringComparison.OrdinalIgnoreCase) && trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase))                return trimmedSegment.Substring(1, trimmedSegment.Length - 2);            else                return trimmedSegment;        }        #endregion    }}
 |