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
/// <summary>
/// Parses the <paramref name="data"/> provided into either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.
/// </summary>
/// <param name="data">A string containing the HTTP message to parse.</param>
/// <returns>Either a <see cref="HttpRequestMessage"/> or <see cref="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="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.
/// </summary>
/// <param name="message">A <see cref="HttpRequestMessage"/> or <see cref="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="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 ( Array . Empty < byte > ( ) ) )
{
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="HttpResponseMessage"/> or <see cref="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
}
}