HttpParserBase.cs 11 KB

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