| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 | using System;using System.Collections.Generic;using System.Linq;using System.Net.Http;using System.Text;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 static readonly string[] LineTerminators = new string[] { "\r\n", "\n" };		private static readonly char[] SeparatorCharacters = new char[] { ',', ';' };		#endregion		#region Public Methods		/// <summary>		/// Parses the <paramref name="data"/> provided into either a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object.		/// </summary>		/// <param name="data">A string containing the HTTP message to parse.</param>		/// <returns>Either a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.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="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object.		/// </summary>		/// <param name="message">A <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.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="System.Net.Http.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 HttpContent Parse(T message, System.Net.Http.Headers.HttpHeaders headers, string data)		{			if (data == null) throw new ArgumentNullException("data");			if (data.Length == 0) throw new ArgumentException("data cannot be an empty string.", "data");			if (!LineTerminators.Any(data.Contains)) throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", "data");			HttpContent retVal = null;			try			{				var contentStream = new System.IO.MemoryStream();				try				{					retVal = new StreamContent(contentStream);					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);					int lineIndex = ParseHeaders(headers, retVal.Headers, lines);					if (lineIndex < lines.Length - 1)					{						//Read rest of any remaining data as content.						if (lineIndex < lines.Length - 1)						{							//This is inefficient in multiple ways, but not sure of a good way of correcting. Revisit.							var body = System.Text.UTF8Encoding.UTF8.GetBytes(String.Join(null, lines, lineIndex, lines.Length - lineIndex));							contentStream.Write(body, 0, body.Length);							contentStream.Seek(0, System.IO.SeekOrigin.Begin);						}					}				}				catch				{					if (contentStream != null)						contentStream.Dispose();					throw;				}			}			catch			{				if (retVal != null)					retVal.Dispose();				throw;			}			return retVal;		}		/// <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="System.Net.Http.HttpResponseMessage"/> or <see cref="System.Net.Http.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 static Version ParseHttpVersion(string versionData)		{			if (versionData == null) throw new ArgumentNullException("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.", "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 static 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 static 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	}}
 |