using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using EmbedIO.Utilities;
namespace EmbedIO
{
///
/// Provides extension methods for types implementing .
///
public static class HttpRequestExtensions
{
///
/// Returns a string representing the remote IP address and port of an interface.
/// This method can be called even on a interface, or one that has no
/// remote end point, or no remote address; it will always return a non-,
/// non-empty string.
///
/// The on which this method is called.
///
/// If is , or its RemoteEndPoint
/// is , the string "<null>; otherwise, the remote end point's
/// Address (or the string "<???>" if it is )
/// followed by a colon and the Port number.
///
public static string SafeGetRemoteEndpointStr(this IHttpRequest @this)
{
var endPoint = @this?.RemoteEndPoint;
return endPoint == null
? ""
: $"{endPoint.Address?.ToString() ?? "??>"}:{endPoint.Port.ToString(CultureInfo.InvariantCulture)}";
}
///
/// Attempts to proactively negotiate a compression method for a response,
/// based on a request's Accept-Encoding header (or lack of it).
///
/// The on which this method is called.
/// if sending compressed data is preferred over
/// sending non-compressed data; otherwise, .
/// When this method returns, the compression method to use for the response,
/// if content negotiation is successful. This parameter is passed uninitialized.
/// When this method returns, a callback that prepares data in an
/// according to the result of content negotiation. This parameter is passed uninitialized.
/// if content negotiation is successful;
/// otherwise, .
///
/// If this method returns , the callback
/// will set appropriate response headers to reflect the results of content negotiation.
/// If this method returns , the callback
/// will throw a to send a 406 Not Acceptable response
/// with the Vary header set to Accept-Encoding,
/// so that the client may know the reason why the request has been rejected.
/// If has noAccept-Encoding header, this method
/// always returns and sets
/// to .
///
///
public static bool TryNegotiateContentEncoding(
this IHttpRequest @this,
bool preferCompression,
out CompressionMethod compressionMethod,
out Action prepareResponse)
{
var acceptedEncodings = new QValueList(true, @this.Headers.GetValues(HttpHeaderNames.AcceptEncoding));
if (!acceptedEncodings.TryNegotiateContentEncoding(preferCompression, out compressionMethod, out var compressionMethodName))
{
prepareResponse = r => throw HttpException.NotAcceptable(HttpHeaderNames.AcceptEncoding);
return false;
}
prepareResponse = r => {
r.Headers.Add(HttpHeaderNames.Vary, HttpHeaderNames.AcceptEncoding);
r.Headers.Set(HttpHeaderNames.ContentEncoding, compressionMethodName);
};
return true;
}
///
/// Checks whether an If-None-Match header exists in a request
/// and, if so, whether it contains a given entity tag.
/// See RFC7232, Section 3.2
/// for a normative reference; however, see the Remarks section for more information
/// about the RFC compliance of this method.
///
/// The on which this method is called.
/// The entity tag.
/// When this method returns, a value that indicates whether an
/// If-None-Match header is present in , regardless of the method's
/// return value. This parameter is passed uninitialized.
/// if an If-None-Match header is present in
/// and one of the entity tags listed in it is equal to ;
/// otherwise.
///
/// RFC7232, Section 3.2
/// states that a weak comparison function (as defined in
/// RFC7232, Section 2.3.2)
/// must be used for If-None-Match. That would mean parsing every entity tag, at least minimally,
/// to determine whether it is a "weak" or "strong" tag. Since EmbedIO currently generates only
/// "strong" tags, this method uses the default string comparer instead.
/// The behavior of this method is thus not, strictly speaking, RFC7232-compliant;
/// it works, though, with entity tags generated by EmbedIO.
///
public static bool CheckIfNoneMatch(this IHttpRequest @this, string entityTag, out bool headerExists)
{
var values = @this.Headers.GetValues(HttpHeaderNames.IfNoneMatch);
if (values == null)
{
headerExists = false;
return false;
}
headerExists = true;
return values.Select(t => t.Trim()).Contains(entityTag);
}
// Check whether the If-Modified-Since request header exists
// and specifies a date and time more recent than or equal to
// the date and time of last modification of the requested resource.
// RFC7232, Section 3.3
///
/// Checks whether an If-Modified-Since header exists in a request
/// and, if so, whether its value is a date and time more recent or equal to
/// a given .
/// See RFC7232, Section 3.3
/// for a normative reference.
///
/// The on which this method is called.
/// A date and time value, in Coordinated Universal Time,
/// expressing the last time a resource was modified.
/// When this method returns, a value that indicates whether an
/// If-Modified-Since header is present in , regardless of the method's
/// return value. This parameter is passed uninitialized.
/// if an If-Modified-Since header is present in
/// and its value is a date and time more recent or equal to ;
/// otherwise.
public static bool CheckIfModifiedSince(this IHttpRequest @this, DateTime lastModifiedUtc, out bool headerExists)
{
var value = @this.Headers.Get(HttpHeaderNames.IfModifiedSince);
if (value == null)
{
headerExists = false;
return false;
}
headerExists = true;
return HttpDate.TryParse(value, out var dateTime)
&& dateTime.UtcDateTime >= lastModifiedUtc;
}
// Checks the Range request header to tell whether to send
// a "206 Partial Content" response.
///
/// Checks whether a Range header exists in a request
/// and, if so, determines whether it is possible to send a 206 Partial Content response.
/// See RFC7233
/// for a normative reference; however, see the Remarks section for more information
/// about the RFC compliance of this method.
///
/// The on which this method is called.
/// The total length, in bytes, of the response entity, i.e.
/// what would be sent in a 200 OK response.
/// An entity tag representing the response entity. This value is checked against
/// the If-Range header, if it is present.
/// The date and time value, in Coordinated Universal Time,
/// expressing the last modification time of the resource entity. This value is checked against
/// the If-Range header, if it is present.
/// When this method returns , the start of the requested byte range.
/// This parameter is passed uninitialized.
///
/// When this method returns , the upper bound of the requested byte range.
/// This parameter is passed uninitialized.
/// Note that the upper bound of a range is NOT the sum of the range's start and length;
/// for example, a range expressed as bytes=0-99 has a start of 0, an upper bound of 99,
/// and a length of 100 bytes.
///
///
/// This method returns if the following conditions are satisfied:
///
/// - >the request's HTTP method is GET;
/// - >a Range header is present in the request;
/// - >either no If-Range header is present in the request, or it
/// specifies an entity tag equal to , or a UTC date and time
/// equal to ;
/// - >the Range header specifies exactly one range;
/// - >the specified range is entirely contained in the range from 0 to - 1.
///
/// If the last condition is not satisfied, i.e. the specified range start and/or upper bound
/// are out of the range from 0 to - 1, this method does not return;
/// it throws a instead.
/// If any of the other conditions are not satisfied, this method returns .
///
///
/// According to RFC7233, Section 3.1,
/// there are several conditions under which a server may ignore or reject a range request; therefore,
/// clients are (or should be) prepared to receive a 200 OK response with the whole response
/// entity instead of the requested range(s). For this reason, until the generation of
/// multipart/byteranges responses is implemented in EmbedIO, this method will ignore
/// range requests specifying more than one range, even if this behavior is not, strictly speaking,
/// RFC7233-compliant.
/// To make clients aware that range requests are accepted for a resource, every 200 OK
/// (or 304 Not Modified) response for the same resource should include an Accept-Ranges
/// header with the string bytes as value.
///
public static bool IsRangeRequest(this IHttpRequest @this, long contentLength, string entityTag, DateTime lastModifiedUtc, out long start, out long upperBound)
{
start = 0;
upperBound = contentLength - 1;
// RFC7233, Section 3.1:
// "A server MUST ignore a Range header field received with a request method other than GET."
if (@this.HttpVerb != HttpVerbs.Get)
return false;
// No Range header, no partial content.
var rangeHeader = @this.Headers.Get(HttpHeaderNames.Range);
if (rangeHeader == null)
return false;
// Ignore the Range header if there is no If-Range header
// or if the If-Range header specifies a non-matching validator.
// RFC7233, Section 3.2: "If the validator given in the If-Range header field matches the
// current validator for the selected representation of the target
// resource, then the server SHOULD process the Range header field as
// requested.If the validator does not match, the server MUST ignore
// the Range header field.Note that this comparison by exact match,
// including when the validator is an HTTP-date, differs from the
// "earlier than or equal to" comparison used when evaluating an
// If-Unmodified-Since conditional."
var ifRange = @this.Headers.Get(HttpHeaderNames.IfRange)?.Trim();
if (ifRange != null && ifRange != entityTag)
{
if (!HttpDate.TryParse(ifRange, out var rangeDate))
return false;
if (rangeDate.UtcDateTime != lastModifiedUtc)
return false;
}
// Ignore the Range request header if it cannot be parsed successfully.
if (!RangeHeaderValue.TryParse(rangeHeader, out var range))
return false;
// EmbedIO does not support multipart/byteranges responses (yet),
// thus ignore range requests that specify one range.
if (range.Ranges.Count != 1)
return false;
var firstRange = range.Ranges.First();
start = firstRange.From ?? 0L;
upperBound = firstRange.To ?? contentLength - 1;
if (start >= contentLength || upperBound < start || upperBound >= contentLength)
throw HttpException.RangeNotSatisfiable(contentLength);
return true;
}
}
}