HttpParserBase.cs 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Net.Http;
  5. using System.Text;
  6. namespace Rssdp.Infrastructure
  7. {
  8. /// <summary>
  9. /// A base class for the <see cref="HttpResponseParser"/> and <see cref="HttpRequestParser"/> classes. Not intended for direct use.
  10. /// </summary>
  11. /// <typeparam name="T"></typeparam>
  12. public abstract class HttpParserBase<T> where T : new()
  13. {
  14. #region Fields
  15. private static readonly string[] LineTerminators = new string[] { "\r\n", "\n" };
  16. private static readonly char[] SeparatorCharacters = new char[] { ',', ';' };
  17. #endregion
  18. #region Public Methods
  19. /// <summary>
  20. /// Parses the <paramref name="data"/> provided into either a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object.
  21. /// </summary>
  22. /// <param name="data">A string containing the HTTP message to parse.</param>
  23. /// <returns>Either a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object containing the parsed data.</returns>
  24. public abstract T Parse(string data);
  25. /// <summary>
  26. /// 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.
  27. /// </summary>
  28. /// <param name="message">A <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object representing the parsed message.</param>
  29. /// <param name="headers">A reference to the <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the <paramref name="message"/> object.</param>
  30. /// <param name="data">A string containing the data to be parsed.</param>
  31. /// <returns>An <see cref="System.Net.Http.HttpContent"/> object containing the content of the parsed message.</returns>
  32. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification="Honestly, it's fine. MemoryStream doesn't mind.")]
  33. protected virtual HttpContent Parse(T message, System.Net.Http.Headers.HttpHeaders headers, string data)
  34. {
  35. if (data == null) throw new ArgumentNullException("data");
  36. if (data.Length == 0) throw new ArgumentException("data cannot be an empty string.", "data");
  37. if (!LineTerminators.Any(data.Contains)) throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", "data");
  38. HttpContent retVal = null;
  39. try
  40. {
  41. var contentStream = new System.IO.MemoryStream();
  42. try
  43. {
  44. retVal = new StreamContent(contentStream);
  45. var lines = data.Split(LineTerminators, StringSplitOptions.None);
  46. //First line is the 'request' line containing http protocol details like method, uri, http version etc.
  47. ParseStatusLine(lines[0], message);
  48. int lineIndex = ParseHeaders(headers, retVal.Headers, lines);
  49. if (lineIndex < lines.Length - 1)
  50. {
  51. //Read rest of any remaining data as content.
  52. if (lineIndex < lines.Length - 1)
  53. {
  54. //This is inefficient in multiple ways, but not sure of a good way of correcting. Revisit.
  55. var body = System.Text.UTF8Encoding.UTF8.GetBytes(String.Join(null, lines, lineIndex, lines.Length - lineIndex));
  56. contentStream.Write(body, 0, body.Length);
  57. contentStream.Seek(0, System.IO.SeekOrigin.Begin);
  58. }
  59. }
  60. }
  61. catch
  62. {
  63. if (contentStream != null)
  64. contentStream.Dispose();
  65. throw;
  66. }
  67. }
  68. catch
  69. {
  70. if (retVal != null)
  71. retVal.Dispose();
  72. throw;
  73. }
  74. return retVal;
  75. }
  76. /// <summary>
  77. /// 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"/>.
  78. /// </summary>
  79. /// <param name="data">The first line of the HTTP message to be parsed.</param>
  80. /// <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>
  81. protected abstract void ParseStatusLine(string data, T message);
  82. /// <summary>
  83. /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false).
  84. /// </summary>
  85. /// <param name="headerName">A string containing the name of the header to return the type of.</param>
  86. protected abstract bool IsContentHeader(string headerName);
  87. /// <summary>
  88. /// Parses the HTTP version text from an HTTP request or response status line and returns a <see cref="Version"/> object representing the parsed values.
  89. /// </summary>
  90. /// <param name="versionData">A string containing the HTTP version, from the message status line.</param>
  91. /// <returns>A <see cref="Version"/> object containing the parsed version data.</returns>
  92. protected static Version ParseHttpVersion(string versionData)
  93. {
  94. if (versionData == null) throw new ArgumentNullException("versionData");
  95. var versionSeparatorIndex = versionData.IndexOf('/');
  96. if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length) throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", "versionData");
  97. return Version.Parse(versionData.Substring(versionSeparatorIndex + 1));
  98. }
  99. #endregion
  100. #region Private Methods
  101. /// <summary>
  102. /// Parses a line from an HTTP request or response message containing a header name and value pair.
  103. /// </summary>
  104. /// <param name="line">A string containing the data to be parsed.</param>
  105. /// <param name="headers">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection to which the parsed header will be added.</param>
  106. /// <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>
  107. private void ParseHeader(string line, System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders)
  108. {
  109. //Header format is
  110. //name: value
  111. var headerKeySeparatorIndex = line.IndexOf(":", StringComparison.OrdinalIgnoreCase);
  112. var headerName = line.Substring(0, headerKeySeparatorIndex).Trim();
  113. var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim();
  114. //Not sure how to determine where request headers and and content headers begin,
  115. //at least not without a known set of headers (general headers first the content headers)
  116. //which seems like a bad way of doing it. So we'll assume if it's a known content header put it there
  117. //else use request headers.
  118. var values = ParseValues(headerValue);
  119. var headersToAddTo = IsContentHeader(headerName) ? contentHeaders : headers;
  120. if (values.Count > 1)
  121. headersToAddTo.TryAddWithoutValidation(headerName, values);
  122. else
  123. headersToAddTo.TryAddWithoutValidation(headerName, values.First());
  124. }
  125. private int ParseHeaders(System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders, string[] lines)
  126. {
  127. //Blank line separates headers from content, so read headers until we find blank line.
  128. int lineIndex = 1;
  129. string line = null, nextLine = null;
  130. while (lineIndex + 1 < lines.Length && !String.IsNullOrEmpty((line = lines[lineIndex++])))
  131. {
  132. //If the following line starts with space or tab (or any whitespace), it is really part of this header but split for human readability.
  133. //Combine these lines into a single comma separated style header for easier parsing.
  134. while (lineIndex < lines.Length && !String.IsNullOrEmpty((nextLine = lines[lineIndex])))
  135. {
  136. if (nextLine.Length > 0 && Char.IsWhiteSpace(nextLine[0]))
  137. {
  138. line += "," + nextLine.TrimStart();
  139. lineIndex++;
  140. }
  141. else
  142. break;
  143. }
  144. ParseHeader(line, headers, contentHeaders);
  145. }
  146. return lineIndex;
  147. }
  148. private static IList<string> ParseValues(string headerValue)
  149. {
  150. // This really should be better and match the HTTP 1.1 spec,
  151. // but this should actually be good enough for SSDP implementations
  152. // I think.
  153. var values = new List<string>();
  154. if (headerValue == "\"\"")
  155. {
  156. values.Add(String.Empty);
  157. return values;
  158. }
  159. var indexOfSeparator = headerValue.IndexOfAny(SeparatorCharacters);
  160. if (indexOfSeparator <= 0)
  161. values.Add(headerValue);
  162. else
  163. {
  164. var segments = headerValue.Split(SeparatorCharacters);
  165. if (headerValue.Contains("\""))
  166. {
  167. for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++)
  168. {
  169. var segment = segments[segmentIndex];
  170. if (segment.Trim().StartsWith("\"", StringComparison.OrdinalIgnoreCase))
  171. segment = CombineQuotedSegments(segments, ref segmentIndex, segment);
  172. values.Add(segment);
  173. }
  174. }
  175. else
  176. values.AddRange(segments);
  177. }
  178. return values;
  179. }
  180. private static string CombineQuotedSegments(string[] segments, ref int segmentIndex, string segment)
  181. {
  182. var trimmedSegment = segment.Trim();
  183. for (int index = segmentIndex; index < segments.Length; index++)
  184. {
  185. if (trimmedSegment == "\"\"" ||
  186. (
  187. trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase)
  188. && !trimmedSegment.EndsWith("\"\"", StringComparison.OrdinalIgnoreCase)
  189. && !trimmedSegment.EndsWith("\\\"", StringComparison.OrdinalIgnoreCase))
  190. )
  191. {
  192. segmentIndex = index;
  193. return trimmedSegment.Substring(1, trimmedSegment.Length - 2);
  194. }
  195. if (index + 1 < segments.Length)
  196. trimmedSegment += "," + segments[index + 1].TrimEnd();
  197. }
  198. segmentIndex = segments.Length;
  199. if (trimmedSegment.StartsWith("\"", StringComparison.OrdinalIgnoreCase) && trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase))
  200. return trimmedSegment.Substring(1, trimmedSegment.Length - 2);
  201. else
  202. return trimmedSegment;
  203. }
  204. #endregion
  205. }
  206. }