jellyfin/RSSDP/HttpParserBase.cs

231 lines
11 KiB
C#
Raw Normal View History

using System;
2016-10-29 22:22:20 +00:00
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
namespace Rssdp.Infrastructure
{
2018-09-12 17:26:21 +00:00
/// <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()
{
private readonly string[] LineTerminators = new string[] { "\r\n", "\n" };
private readonly char[] SeparatorCharacters = new char[] { ',', ';' };
/// <summary>
2019-01-13 20:37:13 +00:00
/// Parses the <paramref name="data"/> provided into either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.
2018-09-12 17:26:21 +00:00
/// </summary>
/// <param name="data">A string containing the HTTP message to parse.</param>
2019-01-13 20:37:13 +00:00
/// <returns>Either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object containing the parsed data.</returns>
2018-09-12 17:26:21 +00:00
public abstract T Parse(string data);
/// <summary>
2019-01-13 20:37:13 +00:00
/// Parses a string containing either an HTTP request or response into a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.
2018-09-12 17:26:21 +00:00
/// </summary>
2019-01-13 20:37:13 +00:00
/// <param name="message">A <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object representing the parsed message.</param>
2018-09-12 17:26:21 +00:00
/// <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>
2019-01-13 20:37:13 +00:00
/// <returns>An <see cref="HttpContent"/> object containing the content of the parsed message.</returns>
2018-09-12 17:26:21 +00:00
[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)
{
2020-06-20 08:35:29 +00:00
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));
}
2018-09-12 17:26:21 +00:00
2019-02-24 02:16:19 +00:00
using (var retVal = new ByteArrayContent(Array.Empty<byte>()))
2018-09-12 17:26:21 +00:00
{
var lines = data.Split(LineTerminators, StringSplitOptions.None);
2020-06-14 09:11:11 +00:00
// First line is the 'request' line containing http protocol details like method, uri, http version etc.
2018-09-12 17:26:21 +00:00
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>
2019-01-13 20:37:13 +00:00
/// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param>
2018-09-12 17:26:21 +00:00
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)
{
2020-06-20 08:35:29 +00:00
if (versionData == null)
{
throw new ArgumentNullException(nameof(versionData));
}
2018-09-12 17:26:21 +00:00
2021-11-15 14:57:07 +00:00
var versionSeparatorIndex = versionData.IndexOf('/', StringComparison.Ordinal);
2020-06-20 08:35:29 +00:00
if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length)
{
throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", nameof(versionData));
}
2018-09-12 17:26:21 +00:00
return Version.Parse(versionData.Substring(versionSeparatorIndex + 1));
}
/// <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)
{
2020-06-14 09:11:11 +00:00
// Header format is
// name: value
2021-11-15 14:57:07 +00:00
var headerKeySeparatorIndex = line.IndexOf(':', StringComparison.Ordinal);
2018-09-12 17:26:21 +00:00
var headerName = line.Substring(0, headerKeySeparatorIndex).Trim();
var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim();
2020-11-18 13:46:14 +00:00
// Not sure how to determine where request headers and content headers begin,
2020-06-14 09:11:11 +00:00
// 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.
2018-09-12 17:26:21 +00:00
var values = ParseValues(headerValue);
var headersToAddTo = IsContentHeader(headerName) ? contentHeaders : headers;
if (values.Count > 1)
2020-06-20 09:12:36 +00:00
{
2018-09-12 17:26:21 +00:00
headersToAddTo.TryAddWithoutValidation(headerName, values);
2020-06-20 09:12:36 +00:00
}
2018-09-12 17:26:21 +00:00
else
2020-06-20 09:12:36 +00:00
{
2020-11-14 15:28:49 +00:00
headersToAddTo.TryAddWithoutValidation(headerName, values[0]);
2020-06-20 09:12:36 +00:00
}
2018-09-12 17:26:21 +00:00
}
private int ParseHeaders(System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders, string[] lines)
{
2020-06-14 09:11:11 +00:00
// Blank line separates headers from content, so read headers until we find blank line.
2018-09-12 17:26:21 +00:00
int lineIndex = 1;
string line = null, nextLine = null;
while (lineIndex + 1 < lines.Length && !String.IsNullOrEmpty((line = lines[lineIndex++])))
{
2020-06-14 09:11:11 +00:00
// 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.
2018-09-12 17:26:21 +00:00
while (lineIndex < lines.Length && !String.IsNullOrEmpty((nextLine = lines[lineIndex])))
{
if (nextLine.Length > 0 && Char.IsWhiteSpace(nextLine[0]))
{
line += "," + nextLine.TrimStart();
lineIndex++;
}
else
2020-06-20 09:19:16 +00:00
{
2018-09-12 17:26:21 +00:00
break;
2020-06-20 09:19:16 +00:00
}
2018-09-12 17:26:21 +00:00
}
ParseHeader(line, headers, contentHeaders);
}
2020-06-15 21:43:52 +00:00
2018-09-12 17:26:21 +00:00
return lineIndex;
}
2020-11-14 15:28:49 +00:00
private List<string> ParseValues(string headerValue)
2018-09-12 17:26:21 +00:00
{
// 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 == "\"\"")
{
2020-11-14 15:28:49 +00:00
values.Add(string.Empty);
2018-09-12 17:26:21 +00:00
return values;
}
var indexOfSeparator = headerValue.IndexOfAny(SeparatorCharacters);
if (indexOfSeparator <= 0)
2020-06-20 09:12:36 +00:00
{
2018-09-12 17:26:21 +00:00
values.Add(headerValue);
2020-06-20 09:12:36 +00:00
}
2018-09-12 17:26:21 +00:00
else
{
var segments = headerValue.Split(SeparatorCharacters);
2021-11-15 14:57:07 +00:00
if (headerValue.Contains('"', StringComparison.Ordinal))
2018-09-12 17:26:21 +00:00
{
for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++)
{
var segment = segments[segmentIndex];
if (segment.Trim().StartsWith("\"", StringComparison.OrdinalIgnoreCase))
2020-06-20 09:12:36 +00:00
{
2018-09-12 17:26:21 +00:00
segment = CombineQuotedSegments(segments, ref segmentIndex, segment);
2020-06-20 09:12:36 +00:00
}
2018-09-12 17:26:21 +00:00
values.Add(segment);
}
}
else
2020-06-20 09:19:16 +00:00
{
2018-09-12 17:26:21 +00:00
values.AddRange(segments);
2020-06-20 09:19:16 +00:00
}
2018-09-12 17:26:21 +00:00
}
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)
2020-06-20 09:12:36 +00:00
{
2018-09-12 17:26:21 +00:00
trimmedSegment += "," + segments[index + 1].TrimEnd();
2020-06-20 09:12:36 +00:00
}
2018-09-12 17:26:21 +00:00
}
segmentIndex = segments.Length;
if (trimmedSegment.StartsWith("\"", StringComparison.OrdinalIgnoreCase) && trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase))
2020-06-20 09:12:36 +00:00
{
2018-09-12 17:26:21 +00:00
return trimmedSegment.Substring(1, trimmedSegment.Length - 2);
2020-06-20 09:12:36 +00:00
}
2018-09-12 17:26:21 +00:00
else
2020-06-20 09:19:16 +00:00
{
2018-09-12 17:26:21 +00:00
return trimmedSegment;
2020-06-20 09:19:16 +00:00
}
2018-09-12 17:26:21 +00:00
}
}
2019-02-24 02:16:19 +00:00
}