jellyfin-server/RSSDP/HttpParserBase.cs
Erwin de Haan ec1f5dc317 Mayor code cleanup
Add Argument*Exceptions now use proper nameof operators.

Added exception messages to quite a few Argument*Exceptions.

Fixed rethorwing to be proper syntax.

Added a ton of null checkes. (This is only a start, there are about 500 places that need proper null handling)

Added some TODOs to log certain exceptions.

Fix sln again.

Fixed all AssemblyInfo's and added proper copyright (where I could find them)

We live in *current year*.

Fixed the use of braces.

Fixed a ton of properties, and made a fair amount of functions static that should be and can be static.

Made more Methods that should be static static.

You can now use static to find bad functions!

Removed unused variable. And added one more proper XML comment.
2019-01-10 20:38:53 +01:00

212 lines
10 KiB
C#

using System;
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 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(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(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(nameof(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.", nameof(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
}
}