// 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 /// <summary> /// <para>Checks whether an <c>If-Modified-Since</c> header exists in a request /// and, if so, whether its value is a date and time more recent or equal to /// a given <see cref="DateTime"/>.</para> /// <para>See <see href="https://tools.ietf.org/html/rfc7232#section-3.3">RFC7232, Section 3.3</see> /// for a normative reference.</para> /// </summary> /// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param> /// <param name="lastModifiedUtc">A date and time value, in Coordinated Universal Time, /// expressing the last time a resource was modified.</param> /// <param name="headerExists">When this method returns, a value that indicates whether an /// <c>If-Modified-Since</c> header is present in <paramref name="this"/>, regardless of the method's /// return value. This parameter is passed uninitialized.</param> /// <returns><see langword="true"/> if an <c>If-Modified-Since</c> header is present in /// <paramref name="this"/> and its value is a date and time more recent or equal to <paramref name="lastModifiedUtc"/>; /// <see langword="false"/> otherwise.</returns> 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); }
/// <summary>Creates a <see cref="CookieList"/> by parsing /// the value of one or more <c>Cookie</c> or <c>Set-Cookie</c> headers.</summary> /// <param name="headerValue">The value, or comma-separated list of values, /// of the header or headers.</param> /// <returns>A newly-created instance of <see cref="CookieList"/>.</returns> public static CookieList Parse(string headerValue) { var cookies = new CookieList(); Cookie cookie = null; var pairs = SplitCookieHeaderValue(headerValue); for (var i = 0; i < pairs.Length; i++) { var pair = pairs[i].Trim(); if (pair.Length == 0) { continue; } if (pair.StartsWith("version", StringComparison.OrdinalIgnoreCase) && cookie != null) { cookie.Version = int.Parse(GetValue(pair, true), CultureInfo.InvariantCulture); } else if (pair.StartsWith("expires", StringComparison.OrdinalIgnoreCase) && cookie != null) { var buff = new StringBuilder(GetValue(pair), 32); if (i < pairs.Length - 1) { buff.AppendFormat(CultureInfo.InvariantCulture, ", {0}", pairs[++i].Trim()); } if (!HttpDate.TryParse(buff.ToString(), out var expires)) { expires = DateTimeOffset.Now; } if (cookie.Expires == DateTime.MinValue) { cookie.Expires = expires.LocalDateTime; } } else if (pair.StartsWith("max-age", StringComparison.OrdinalIgnoreCase) && cookie != null) { var max = int.Parse(GetValue(pair, true), CultureInfo.InvariantCulture); cookie.Expires = DateTime.Now.AddSeconds(max); } else if (pair.StartsWith("path", StringComparison.OrdinalIgnoreCase) && cookie != null) { cookie.Path = GetValue(pair); } else if (pair.StartsWith("domain", StringComparison.OrdinalIgnoreCase) && cookie != null) { cookie.Domain = GetValue(pair); } else if (pair.StartsWith("port", StringComparison.OrdinalIgnoreCase) && cookie != null) { cookie.Port = pair.Equals("port", StringComparison.OrdinalIgnoreCase) ? "\"\"" : GetValue(pair); } else if (pair.StartsWith("comment", StringComparison.OrdinalIgnoreCase) && cookie != null) { cookie.Comment = WebUtility.UrlDecode(GetValue(pair)); } else if (pair.StartsWith("commenturl", StringComparison.OrdinalIgnoreCase) && cookie != null) { cookie.CommentUri = UriUtility.StringToUri(GetValue(pair, true)); } else if (pair.StartsWith("discard", StringComparison.OrdinalIgnoreCase) && cookie != null) { cookie.Discard = true; } else if (pair.StartsWith("secure", StringComparison.OrdinalIgnoreCase) && cookie != null) { cookie.Secure = true; } else if (pair.StartsWith("httponly", StringComparison.OrdinalIgnoreCase) && cookie != null) { cookie.HttpOnly = true; } else { if (cookie != null) { cookies.Add(cookie); } cookie = ParseCookie(pair); } } if (cookie != null) { cookies.Add(cookie); } return(cookies); }
// Checks the Range request header to tell whether to send // a "206 Partial Content" response. /// <summary> /// <para>Checks whether a <c>Range</c> header exists in a request /// and, if so, determines whether it is possible to send a <c>206 Partial Content</c> response.</para> /// <para>See <see href="https://tools.ietf.org/html/rfc7233">RFC7233</see> /// for a normative reference; however, see the Remarks section for more information /// about the RFC compliance of this method.</para> /// </summary> /// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param> /// <param name="contentLength">The total length, in bytes, of the response entity, i.e. /// what would be sent in a <c>200 OK</c> response.</param> /// <param name="entityTag">An entity tag representing the response entity. This value is checked against /// the <c>If-Range</c> header, if it is present.</param> /// <param name="lastModifiedUtc">The date and time value, in Coordinated Universal Time, /// expressing the last modification time of the resource entity. This value is checked against /// the <c>If-Range</c> header, if it is present.</param> /// <param name="start">When this method returns <see langword="true"/>, the start of the requested byte range. /// This parameter is passed uninitialized.</param> /// <param name="upperBound"> /// <para>When this method returns <see langword="true"/>, the upper bound of the requested byte range. /// This parameter is passed uninitialized.</para> /// <para>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 <c>bytes=0-99</c> has a start of 0, an upper bound of 99, /// and a length of 100 bytes.</para> /// </param> /// <returns> /// <para>This method returns <see langword="true"/> if the following conditions are satisfied:</para> /// <list type="bullet"> /// <item><description>>the request's HTTP method is <c>GET</c>;</description></item> /// <item><description>>a <c>Range</c> header is present in the request;</description></item> /// <item><description>>either no <c>If-Range</c> header is present in the request, or it /// specifies an entity tag equal to <paramref name="entityTag"/>, or a UTC date and time /// equal to <paramref name="lastModifiedUtc"/>;</description></item> /// <item><description>>the <c>Range</c> header specifies exactly one range;</description></item> /// <item><description>>the specified range is entirely contained in the range from 0 to <paramref name="contentLength"/> - 1.</description></item> /// </list> /// <para>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 <paramref name="contentLength"/> - 1, this method does not return; /// it throws a <see cref="HttpRangeNotSatisfiableException"/> instead.</para> /// <para>If any of the other conditions are not satisfied, this method returns <see langword="false"/>.</para> /// </returns> /// <remarks> /// <para>According to <see href="https://tools.ietf.org/html/rfc7233#section-3.1">RFC7233, Section 3.1</see>, /// 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 <c>200 OK</c> response with the whole response /// entity instead of the requested range(s). For this reason, until the generation of /// <c>multipart/byteranges</c> 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.</para> /// <para>To make clients aware that range requests are accepted for a resource, every <c>200 OK</c> /// (or <c>304 Not Modified</c>) response for the same resource should include an <c>Accept-Ranges</c> /// header with the string <c>bytes</c> as value.</para> /// </remarks> 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); }