aboutsummaryrefslogtreecommitdiff
path: root/RSSDP/HttpParserBase.cs
diff options
context:
space:
mode:
authorstefan <stefan@hegedues.at>2018-09-12 19:26:21 +0200
committerstefan <stefan@hegedues.at>2018-09-12 19:26:21 +0200
commit48facb797ed912e4ea6b04b17d1ff190ac2daac4 (patch)
tree8dae77a31670a888d733484cb17dd4077d5444e8 /RSSDP/HttpParserBase.cs
parentc32d8656382a0eacb301692e0084377fc433ae9b (diff)
Update to 3.5.2 and .net core 2.1
Diffstat (limited to 'RSSDP/HttpParserBase.cs')
-rw-r--r--RSSDP/HttpParserBase.cs438
1 files changed, 203 insertions, 235 deletions
diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs
index 7934419b0..e841feab3 100644
--- a/RSSDP/HttpParserBase.cs
+++ b/RSSDP/HttpParserBase.cs
@@ -3,242 +3,210 @@ 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 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
-
- }
+ /// <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
+
+ private static byte[] EmptyByteArray = new byte[]{};
+
+ /// <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 void 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");
+
+ using (var retVal = new ByteArrayContent(EmptyByteArray))
+ {
+ 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="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 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 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
+
+ }
} \ No newline at end of file