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; } } }