using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; namespace Rssdp.Infrastructure { /// /// A base class for the and classes. Not intended for direct use. /// /// public abstract class HttpParserBase where T : new() { private readonly string[] LineTerminators = new string[] { "\r\n", "\n" }; private readonly char[] SeparatorCharacters = new char[] { ',', ';' }; /// /// Parses the provided into either a or object. /// /// A string containing the HTTP message to parse. /// Either a or object containing the parsed data. public abstract T Parse(string data); /// /// Parses a string containing either an HTTP request or response into a or object. /// /// A or object representing the parsed message. /// A reference to the collection for the object. /// A string containing the data to be parsed. /// An object containing the content of the parsed message. [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())) { 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); } } /// /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the . /// /// The first line of the HTTP message to be parsed. /// Either a or to assign the parsed values to. protected abstract void ParseStatusLine(string data, T message); /// /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false). /// /// A string containing the name of the header to return the type of. protected abstract bool IsContentHeader(string headerName); /// /// Parses the HTTP version text from an HTTP request or response status line and returns a object representing the parsed values. /// /// A string containing the HTTP version, from the message status line. /// A object containing the parsed version data. protected Version ParseHttpVersion(string versionData) { if (versionData == null) { throw new ArgumentNullException(nameof(versionData)); } var versionSeparatorIndex = versionData.IndexOf('/', StringComparison.Ordinal); 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)); } /// /// Parses a line from an HTTP request or response message containing a header name and value pair. /// /// A string containing the data to be parsed. /// A reference to a collection to which the parsed header will be added. /// A reference to a collection for the message content, to which the parsed header will be added. 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.Ordinal); var headerName = line.Substring(0, headerKeySeparatorIndex).Trim(); var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim(); // Not sure how to determine where request headers 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[0]); } } 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 List 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(); 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('"', StringComparison.Ordinal)) { 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); } return trimmedSegment; } } }