Merge source code from aspnet/HttpAbstractions into this repo

This commit is contained in:
Nate McMaster 2018-11-19 21:13:15 -08:00
commit 98190bdaf9
No known key found for this signature in database
GPG Key ID: A778D9601BD78810
326 changed files with 61908 additions and 0 deletions

View File

@ -0,0 +1,107 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Claims;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Contains the result of an Authenticate call
/// </summary>
public class AuthenticateResult
{
protected AuthenticateResult() { }
/// <summary>
/// If a ticket was produced, authenticate was successful.
/// </summary>
public bool Succeeded => Ticket != null;
/// <summary>
/// The authentication ticket.
/// </summary>
public AuthenticationTicket Ticket { get; protected set; }
/// <summary>
/// Gets the claims-principal with authenticated user identities.
/// </summary>
public ClaimsPrincipal Principal => Ticket?.Principal;
/// <summary>
/// Additional state values for the authentication session.
/// </summary>
public AuthenticationProperties Properties { get; protected set; }
/// <summary>
/// Holds failure information from the authentication.
/// </summary>
public Exception Failure { get; protected set; }
/// <summary>
/// Indicates that there was no information returned for this authentication scheme.
/// </summary>
public bool None { get; protected set; }
/// <summary>
/// Indicates that authentication was successful.
/// </summary>
/// <param name="ticket">The ticket representing the authentication result.</param>
/// <returns>The result.</returns>
public static AuthenticateResult Success(AuthenticationTicket ticket)
{
if (ticket == null)
{
throw new ArgumentNullException(nameof(ticket));
}
return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties };
}
/// <summary>
/// Indicates that there was no information returned for this authentication scheme.
/// </summary>
/// <returns>The result.</returns>
public static AuthenticateResult NoResult()
{
return new AuthenticateResult() { None = true };
}
/// <summary>
/// Indicates that there was a failure during authentication.
/// </summary>
/// <param name="failure">The failure exception.</param>
/// <returns>The result.</returns>
public static AuthenticateResult Fail(Exception failure)
{
return new AuthenticateResult() { Failure = failure };
}
/// <summary>
/// Indicates that there was a failure during authentication.
/// </summary>
/// <param name="failure">The failure exception.</param>
/// <param name="properties">Additional state values for the authentication session.</param>
/// <returns>The result.</returns>
public static AuthenticateResult Fail(Exception failure, AuthenticationProperties properties)
{
return new AuthenticateResult() { Failure = failure, Properties = properties };
}
/// <summary>
/// Indicates that there was a failure during authentication.
/// </summary>
/// <param name="failureMessage">The failure message.</param>
/// <returns>The result.</returns>
public static AuthenticateResult Fail(string failureMessage)
=> Fail(new Exception(failureMessage));
/// <summary>
/// Indicates that there was a failure during authentication.
/// </summary>
/// <param name="failureMessage">The failure message.</param>
/// <param name="properties">Additional state values for the authentication session.</param>
/// <returns>The result.</returns>
public static AuthenticateResult Fail(string failureMessage, AuthenticationProperties properties)
=> Fail(new Exception(failureMessage), properties);
}
}

View File

@ -0,0 +1,197 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Extension methods to expose Authentication on HttpContext.
/// </summary>
public static class AuthenticationHttpContextExtensions
{
/// <summary>
/// Extension method for authenticate using the <see cref="AuthenticationOptions.DefaultAuthenticateScheme"/> scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <returns>The <see cref="AuthenticateResult"/>.</returns>
public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context) =>
context.AuthenticateAsync(scheme: null);
/// <summary>
/// Extension method for authenticate.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <returns>The <see cref="AuthenticateResult"/>.</returns>
public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);
/// <summary>
/// Extension method for Challenge.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <returns>The result.</returns>
public static Task ChallengeAsync(this HttpContext context, string scheme) =>
context.ChallengeAsync(scheme, properties: null);
/// <summary>
/// Extension method for authenticate using the <see cref="AuthenticationOptions.DefaultChallengeScheme"/> scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <returns>The task.</returns>
public static Task ChallengeAsync(this HttpContext context) =>
context.ChallengeAsync(scheme: null, properties: null);
/// <summary>
/// Extension method for authenticate using the <see cref="AuthenticationOptions.DefaultChallengeScheme"/> scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <returns>The task.</returns>
public static Task ChallengeAsync(this HttpContext context, AuthenticationProperties properties) =>
context.ChallengeAsync(scheme: null, properties: properties);
/// <summary>
/// Extension method for Challenge.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <returns>The task.</returns>
public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().ChallengeAsync(context, scheme, properties);
/// <summary>
/// Extension method for Forbid.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <returns>The task.</returns>
public static Task ForbidAsync(this HttpContext context, string scheme) =>
context.ForbidAsync(scheme, properties: null);
/// <summary>
/// Extension method for Forbid using the <see cref="AuthenticationOptions.DefaultForbidScheme"/> scheme..
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <returns>The task.</returns>
public static Task ForbidAsync(this HttpContext context) =>
context.ForbidAsync(scheme: null, properties: null);
/// <summary>
/// Extension method for Forbid.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <returns>The task.</returns>
public static Task ForbidAsync(this HttpContext context, AuthenticationProperties properties) =>
context.ForbidAsync(scheme: null, properties: properties);
/// <summary>
/// Extension method for Forbid.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <returns>The task.</returns>
public static Task ForbidAsync(this HttpContext context, string scheme, AuthenticationProperties properties) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().ForbidAsync(context, scheme, properties);
/// <summary>
/// Extension method for SignIn.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="principal">The user.</param>
/// <returns>The task.</returns>
public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal) =>
context.SignInAsync(scheme, principal, properties: null);
/// <summary>
/// Extension method for SignIn using the <see cref="AuthenticationOptions.DefaultSignInScheme"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="principal">The user.</param>
/// <returns>The task.</returns>
public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal) =>
context.SignInAsync(scheme: null, principal: principal, properties: null);
/// <summary>
/// Extension method for SignIn using the <see cref="AuthenticationOptions.DefaultSignInScheme"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="principal">The user.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <returns>The task.</returns>
public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal, AuthenticationProperties properties) =>
context.SignInAsync(scheme: null, principal: principal, properties: properties);
/// <summary>
/// Extension method for SignIn.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="principal">The user.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <returns>The task.</returns>
public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().SignInAsync(context, scheme, principal, properties);
/// <summary>
/// Extension method for SignOut using the <see cref="AuthenticationOptions.DefaultSignOutScheme"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <returns>The task.</returns>
public static Task SignOutAsync(this HttpContext context) => context.SignOutAsync(scheme: null, properties: null);
/// <summary>
/// Extension method for SignOut using the <see cref="AuthenticationOptions.DefaultSignOutScheme"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <returns>The task.</returns>
public static Task SignOutAsync(this HttpContext context, AuthenticationProperties properties) => context.SignOutAsync(scheme: null, properties: properties);
/// <summary>
/// Extension method for SignOut.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <returns>The task.</returns>
public static Task SignOutAsync(this HttpContext context, string scheme) => context.SignOutAsync(scheme, properties: null);
/// <summary>
/// Extension method for SignOut.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <returns></returns>
public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().SignOutAsync(context, scheme, properties);
/// <summary>
/// Extension method for getting the value of an authentication token.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="tokenName">The name of the token.</param>
/// <returns>The value of the token.</returns>
public static Task<string> GetTokenAsync(this HttpContext context, string scheme, string tokenName) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().GetTokenAsync(context, scheme, tokenName);
/// <summary>
/// Extension method for getting the value of an authentication token.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="tokenName">The name of the token.</param>
/// <returns>The value of the token.</returns>
public static Task<string> GetTokenAsync(this HttpContext context, string tokenName) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().GetTokenAsync(context, tokenName);
}
}

View File

@ -0,0 +1,93 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication
{
public class AuthenticationOptions
{
private readonly IList<AuthenticationSchemeBuilder> _schemes = new List<AuthenticationSchemeBuilder>();
/// <summary>
/// Returns the schemes in the order they were added (important for request handling priority)
/// </summary>
public IEnumerable<AuthenticationSchemeBuilder> Schemes => _schemes;
/// <summary>
/// Maps schemes by name.
/// </summary>
public IDictionary<string, AuthenticationSchemeBuilder> SchemeMap { get; } = new Dictionary<string, AuthenticationSchemeBuilder>(StringComparer.Ordinal);
/// <summary>
/// Adds an <see cref="AuthenticationScheme"/>.
/// </summary>
/// <param name="name">The name of the scheme being added.</param>
/// <param name="configureBuilder">Configures the scheme.</param>
public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (configureBuilder == null)
{
throw new ArgumentNullException(nameof(configureBuilder));
}
if (SchemeMap.ContainsKey(name))
{
throw new InvalidOperationException("Scheme already exists: " + name);
}
var builder = new AuthenticationSchemeBuilder(name);
configureBuilder(builder);
_schemes.Add(builder);
SchemeMap[name] = builder;
}
/// <summary>
/// Adds an <see cref="AuthenticationScheme"/>.
/// </summary>
/// <typeparam name="THandler">The <see cref="IAuthenticationHandler"/> responsible for the scheme.</typeparam>
/// <param name="name">The name of the scheme being added.</param>
/// <param name="displayName">The display name for the scheme.</param>
public void AddScheme<THandler>(string name, string displayName) where THandler : IAuthenticationHandler
=> AddScheme(name, b =>
{
b.DisplayName = displayName;
b.HandlerType = typeof(THandler);
});
/// <summary>
/// Used as the fallback default scheme for all the other defaults.
/// </summary>
public string DefaultScheme { get; set; }
/// <summary>
/// Used as the default scheme by <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.
/// </summary>
public string DefaultAuthenticateScheme { get; set; }
/// <summary>
/// Used as the default scheme by <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.
/// </summary>
public string DefaultSignInScheme { get; set; }
/// <summary>
/// Used as the default scheme by <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.
/// </summary>
public string DefaultSignOutScheme { get; set; }
/// <summary>
/// Used as the default scheme by <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.
/// </summary>
public string DefaultChallengeScheme { get; set; }
/// <summary>
/// Used as the default scheme by <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.
/// </summary>
public string DefaultForbidScheme { get; set; }
}
}

View File

@ -0,0 +1,212 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Dictionary used to store state values about the authentication session.
/// </summary>
public class AuthenticationProperties
{
internal const string IssuedUtcKey = ".issued";
internal const string ExpiresUtcKey = ".expires";
internal const string IsPersistentKey = ".persistent";
internal const string RedirectUriKey = ".redirect";
internal const string RefreshKey = ".refresh";
internal const string UtcDateTimeFormat = "r";
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationProperties"/> class.
/// </summary>
public AuthenticationProperties()
: this(items: null, parameters: null)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationProperties"/> class.
/// </summary>
/// <param name="items">State values dictionary to use.</param>
public AuthenticationProperties(IDictionary<string, string> items)
: this(items, parameters: null)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationProperties"/> class.
/// </summary>
/// <param name="items">State values dictionary to use.</param>
/// <param name="parameters">Parameters dictionary to use.</param>
public AuthenticationProperties(IDictionary<string, string> items, IDictionary<string, object> parameters)
{
Items = items ?? new Dictionary<string, string>(StringComparer.Ordinal);
Parameters = parameters ?? new Dictionary<string, object>(StringComparer.Ordinal);
}
/// <summary>
/// State values about the authentication session.
/// </summary>
public IDictionary<string, string> Items { get; }
/// <summary>
/// Collection of parameters that are passed to the authentication handler. These are not intended for
/// serialization or persistence, only for flowing data between call sites.
/// </summary>
public IDictionary<string, object> Parameters { get; }
/// <summary>
/// Gets or sets whether the authentication session is persisted across multiple requests.
/// </summary>
public bool IsPersistent
{
get => GetString(IsPersistentKey) != null;
set => SetString(IsPersistentKey, value ? string.Empty : null);
}
/// <summary>
/// Gets or sets the full path or absolute URI to be used as an http redirect response value.
/// </summary>
public string RedirectUri
{
get => GetString(RedirectUriKey);
set => SetString(RedirectUriKey, value);
}
/// <summary>
/// Gets or sets the time at which the authentication ticket was issued.
/// </summary>
public DateTimeOffset? IssuedUtc
{
get => GetDateTimeOffset(IssuedUtcKey);
set => SetDateTimeOffset(IssuedUtcKey, value);
}
/// <summary>
/// Gets or sets the time at which the authentication ticket expires.
/// </summary>
public DateTimeOffset? ExpiresUtc
{
get => GetDateTimeOffset(ExpiresUtcKey);
set => SetDateTimeOffset(ExpiresUtcKey, value);
}
/// <summary>
/// Gets or sets if refreshing the authentication session should be allowed.
/// </summary>
public bool? AllowRefresh
{
get => GetBool(RefreshKey);
set => SetBool(RefreshKey, value);
}
/// <summary>
/// Get a string value from the <see cref="Items"/> collection.
/// </summary>
/// <param name="key">Property key.</param>
/// <returns>Retrieved value or <c>null</c> if the property is not set.</returns>
public string GetString(string key)
{
return Items.TryGetValue(key, out string value) ? value : null;
}
/// <summary>
/// Set a string value in the <see cref="Items"/> collection.
/// </summary>
/// <param name="key">Property key.</param>
/// <param name="value">Value to set or <c>null</c> to remove the property.</param>
public void SetString(string key, string value)
{
if (value != null)
{
Items[key] = value;
}
else if (Items.ContainsKey(key))
{
Items.Remove(key);
}
}
/// <summary>
/// Get a parameter from the <see cref="Parameters"/> collection.
/// </summary>
/// <typeparam name="T">Parameter type.</typeparam>
/// <param name="key">Parameter key.</param>
/// <returns>Retrieved value or the default value if the property is not set.</returns>
public T GetParameter<T>(string key)
=> Parameters.TryGetValue(key, out var obj) && obj is T value ? value : default;
/// <summary>
/// Set a parameter value in the <see cref="Parameters"/> collection.
/// </summary>
/// <typeparam name="T">Parameter type.</typeparam>
/// <param name="key">Parameter key.</param>
/// <param name="value">Value to set.</param>
public void SetParameter<T>(string key, T value)
=> Parameters[key] = value;
/// <summary>
/// Get a bool value from the <see cref="Items"/> collection.
/// </summary>
/// <param name="key">Property key.</param>
/// <returns>Retrieved value or <c>null</c> if the property is not set.</returns>
protected bool? GetBool(string key)
{
if (Items.TryGetValue(key, out string value) && bool.TryParse(value, out bool boolValue))
{
return boolValue;
}
return null;
}
/// <summary>
/// Set a bool value in the <see cref="Items"/> collection.
/// </summary>
/// <param name="key">Property key.</param>
/// <param name="value">Value to set or <c>null</c> to remove the property.</param>
protected void SetBool(string key, bool? value)
{
if (value.HasValue)
{
Items[key] = value.Value.ToString();
}
else if (Items.ContainsKey(key))
{
Items.Remove(key);
}
}
/// <summary>
/// Get a DateTimeOffset value from the <see cref="Items"/> collection.
/// </summary>
/// <param name="key">Property key.</param>
/// <returns>Retrieved value or <c>null</c> if the property is not set.</returns>
protected DateTimeOffset? GetDateTimeOffset(string key)
{
if (Items.TryGetValue(key, out string value)
&& DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset dateTimeOffset))
{
return dateTimeOffset;
}
return null;
}
/// <summary>
/// Set a DateTimeOffset value in the <see cref="Items"/> collection.
/// </summary>
/// <param name="key">Property key.</param>
/// <param name="value">Value to set or <c>null</c> to remove the property.</param>
protected void SetDateTimeOffset(string key, DateTimeOffset? value)
{
if (value.HasValue)
{
Items[key] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture);
}
else if (Items.ContainsKey(key))
{
Items.Remove(key);
}
}
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// AuthenticationSchemes assign a name to a specific <see cref="IAuthenticationHandler"/>
/// handlerType.
/// </summary>
public class AuthenticationScheme
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="name">The name for the authentication scheme.</param>
/// <param name="displayName">The display name for the authentication scheme.</param>
/// <param name="handlerType">The <see cref="IAuthenticationHandler"/> type that handles this scheme.</param>
public AuthenticationScheme(string name, string displayName, Type handlerType)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (handlerType == null)
{
throw new ArgumentNullException(nameof(handlerType));
}
if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType))
{
throw new ArgumentException("handlerType must implement IAuthenticationHandler.");
}
Name = name;
HandlerType = handlerType;
DisplayName = displayName;
}
/// <summary>
/// The name of the authentication scheme.
/// </summary>
public string Name { get; }
/// <summary>
/// The display name for the scheme. Null is valid and used for non user facing schemes.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// The <see cref="IAuthenticationHandler"/> type that handles this scheme.
/// </summary>
public Type HandlerType { get; }
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Used to build <see cref="AuthenticationScheme"/>s.
/// </summary>
public class AuthenticationSchemeBuilder
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="name">The name of the scheme being built.</param>
public AuthenticationSchemeBuilder(string name)
{
Name = name;
}
/// <summary>
/// The name of the scheme being built.
/// </summary>
public string Name { get; }
/// <summary>
/// The display name for the scheme being built.
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// The <see cref="IAuthenticationHandler"/> type responsible for this scheme.
/// </summary>
public Type HandlerType { get; set; }
/// <summary>
/// Builds the <see cref="AuthenticationScheme"/> instance.
/// </summary>
/// <returns></returns>
public AuthenticationScheme Build() => new AuthenticationScheme(Name, DisplayName, HandlerType);
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Claims;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Contains user identity information as well as additional authentication state.
/// </summary>
public class AuthenticationTicket
{
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationTicket"/> class
/// </summary>
/// <param name="principal">the <see cref="ClaimsPrincipal"/> that represents the authenticated user.</param>
/// <param name="properties">additional properties that can be consumed by the user or runtime.</param>
/// <param name="authenticationScheme">the authentication middleware that was responsible for this ticket.</param>
public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties properties, string authenticationScheme)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
AuthenticationScheme = authenticationScheme;
Principal = principal;
Properties = properties ?? new AuthenticationProperties();
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationTicket"/> class
/// </summary>
/// <param name="principal">the <see cref="ClaimsPrincipal"/> that represents the authenticated user.</param>
/// <param name="authenticationScheme">the authentication middleware that was responsible for this ticket.</param>
public AuthenticationTicket(ClaimsPrincipal principal, string authenticationScheme)
: this(principal, properties: null, authenticationScheme: authenticationScheme)
{ }
/// <summary>
/// Gets the authentication type.
/// </summary>
public string AuthenticationScheme { get; private set; }
/// <summary>
/// Gets the claims-principal with authenticated user identities.
/// </summary>
public ClaimsPrincipal Principal { get; private set; }
/// <summary>
/// Additional state values for the authentication session.
/// </summary>
public AuthenticationProperties Properties { get; private set; }
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Name/Value representing an token.
/// </summary>
public class AuthenticationToken
{
/// <summary>
/// Name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Value.
/// </summary>
public string Value { get; set; }
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Used to capture path info so redirects can be computed properly within an app.Map().
/// </summary>
public interface IAuthenticationFeature
{
/// <summary>
/// The original path base.
/// </summary>
PathString OriginalPathBase { get; set; }
/// <summary>
/// The original path.
/// </summary>
PathString OriginalPath { get; set; }
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Created per request to handle authentication for to a particular scheme.
/// </summary>
public interface IAuthenticationHandler
{
/// <summary>
/// The handler should initialize anything it needs from the request and scheme here.
/// </summary>
/// <param name="scheme">The <see cref="AuthenticationScheme"/> scheme.</param>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <returns></returns>
Task InitializeAsync(AuthenticationScheme scheme, HttpContext context);
/// <summary>
/// Authentication behavior.
/// </summary>
/// <returns>The <see cref="AuthenticateResult"/> result.</returns>
Task<AuthenticateResult> AuthenticateAsync();
/// <summary>
/// Challenge behavior.
/// </summary>
/// <param name="properties">The <see cref="AuthenticationProperties"/> that contains the extra meta-data arriving with the authentication.</param>
/// <returns>A task.</returns>
Task ChallengeAsync(AuthenticationProperties properties);
/// <summary>
/// Forbid behavior.
/// </summary>
/// <param name="properties">The <see cref="AuthenticationProperties"/> that contains the extra meta-data arriving with the authentication.</param>
/// <returns>A task.</returns>
Task ForbidAsync(AuthenticationProperties properties);
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Provides the appropriate IAuthenticationHandler instance for the authenticationScheme and request.
/// </summary>
public interface IAuthenticationHandlerProvider
{
/// <summary>
/// Returns the handler instance that will be used.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="authenticationScheme">The name of the authentication scheme being handled.</param>
/// <returns>The handler instance.</returns>
Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme);
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Used to determine if a handler wants to participate in request processing.
/// </summary>
public interface IAuthenticationRequestHandler : IAuthenticationHandler
{
/// <summary>
/// Returns true if request processing should stop.
/// </summary>
/// <returns></returns>
Task<bool> HandleRequestAsync();
}
}

View File

@ -0,0 +1,86 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Responsible for managing what authenticationSchemes are supported.
/// </summary>
public interface IAuthenticationSchemeProvider
{
/// <summary>
/// Returns all currently registered <see cref="AuthenticationScheme"/>s.
/// </summary>
/// <returns>All currently registered <see cref="AuthenticationScheme"/>s.</returns>
Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync();
/// <summary>
/// Returns the <see cref="AuthenticationScheme"/> matching the name, or null.
/// </summary>
/// <param name="name">The name of the authenticationScheme.</param>
/// <returns>The scheme or null if not found.</returns>
Task<AuthenticationScheme> GetSchemeAsync(string name);
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultAuthenticateScheme"/>.
/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.</returns>
Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync();
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultChallengeScheme"/>.
/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync();
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultForbidScheme"/>.
/// Otherwise, this will fallback to <see cref="GetDefaultChallengeSchemeAsync"/> .
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
Task<AuthenticationScheme> GetDefaultForbidSchemeAsync();
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultSignInScheme"/>.
/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.</returns>
Task<AuthenticationScheme> GetDefaultSignInSchemeAsync();
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultSignOutScheme"/>.
/// Otherwise, this will fallback to <see cref="GetDefaultSignInSchemeAsync"/> .
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync();
/// <summary>
/// Registers a scheme for use by <see cref="IAuthenticationService"/>.
/// </summary>
/// <param name="scheme">The scheme.</param>
void AddScheme(AuthenticationScheme scheme);
/// <summary>
/// Removes a scheme, preventing it from being used by <see cref="IAuthenticationService"/>.
/// </summary>
/// <param name="name">The name of the authenticationScheme being removed.</param>
void RemoveScheme(string name);
/// <summary>
/// Returns the schemes in priority order for request handling.
/// </summary>
/// <returns>The schemes in priority order for request handling</returns>
Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync();
}
}

View File

@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Used to provide authentication.
/// </summary>
public interface IAuthenticationService
{
/// <summary>
/// Authenticate for the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <returns>The result.</returns>
Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme);
/// <summary>
/// Challenge the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
/// <returns>A task.</returns>
Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties);
/// <summary>
/// Forbids the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
/// <returns>A task.</returns>
Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties);
/// <summary>
/// Sign a principal in for the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="principal">The <see cref="ClaimsPrincipal"/> to sign in.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
/// <returns>A task.</returns>
Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
/// <summary>
/// Sign out the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
/// <returns>A task.</returns>
Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties);
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Claims;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Used to determine if a handler supports SignIn.
/// </summary>
public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler
{
/// <summary>
/// Handle sign in.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> user.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/> that contains the extra meta-data arriving with the authentication.</param>
/// <returns>A task.</returns>
Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties);
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Used to determine if a handler supports SignOut.
/// </summary>
public interface IAuthenticationSignOutHandler : IAuthenticationHandler
{
/// <summary>
/// Signout behavior.
/// </summary>
/// <param name="properties">The <see cref="AuthenticationProperties"/> that contains the extra meta-data arriving with the authentication.</param>
/// <returns>A task.</returns>
Task SignOutAsync(AuthenticationProperties properties);
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Claims;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Used by the <see cref="IAuthenticationService"/> for claims transformation.
/// </summary>
public interface IClaimsTransformation
{
/// <summary>
/// Provides a central transformation point to change the specified principal.
/// Note: this will be run on each AuthenticateAsync call, so its safer to
/// return a new ClaimsPrincipal if your transformation is not idempotent.
/// </summary>
/// <param name="principal">The <see cref="ClaimsPrincipal"/> to transform.</param>
/// <returns>The transformed principal.</returns>
Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal);
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<Description>ASP.NET Core common types used by the various authentication components.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;authentication;security</PackageTags>
<EnableApiCheck>false</EnableApiCheck>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Options" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,161 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Extension methods for storing authentication tokens in <see cref="AuthenticationProperties"/>.
/// </summary>
public static class AuthenticationTokenExtensions
{
private static string TokenNamesKey = ".TokenNames";
private static string TokenKeyPrefix = ".Token.";
/// <summary>
/// Stores a set of authentication tokens, after removing any old tokens.
/// </summary>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <param name="tokens">The tokens to store.</param>
public static void StoreTokens(this AuthenticationProperties properties, IEnumerable<AuthenticationToken> tokens)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
if (tokens == null)
{
throw new ArgumentNullException(nameof(tokens));
}
// Clear old tokens first
var oldTokens = properties.GetTokens();
foreach (var t in oldTokens)
{
properties.Items.Remove(TokenKeyPrefix + t.Name);
}
properties.Items.Remove(TokenNamesKey);
var tokenNames = new List<string>();
foreach (var token in tokens)
{
// REVIEW: should probably check that there are no ; in the token name and throw or encode
tokenNames.Add(token.Name);
properties.Items[TokenKeyPrefix+token.Name] = token.Value;
}
if (tokenNames.Count > 0)
{
properties.Items[TokenNamesKey] = string.Join(";", tokenNames.ToArray());
}
}
/// <summary>
/// Returns the value of a token.
/// </summary>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <param name="tokenName">The token name.</param>
/// <returns>The token value.</returns>
public static string GetTokenValue(this AuthenticationProperties properties, string tokenName)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
if (tokenName == null)
{
throw new ArgumentNullException(nameof(tokenName));
}
var tokenKey = TokenKeyPrefix + tokenName;
return properties.Items.ContainsKey(tokenKey)
? properties.Items[tokenKey]
: null;
}
public static bool UpdateTokenValue(this AuthenticationProperties properties, string tokenName, string tokenValue)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
if (tokenName == null)
{
throw new ArgumentNullException(nameof(tokenName));
}
var tokenKey = TokenKeyPrefix + tokenName;
if (!properties.Items.ContainsKey(tokenKey))
{
return false;
}
properties.Items[tokenKey] = tokenValue;
return true;
}
/// <summary>
/// Returns all of the AuthenticationTokens contained in the properties.
/// </summary>
/// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
/// <returns>The authentication toekns.</returns>
public static IEnumerable<AuthenticationToken> GetTokens(this AuthenticationProperties properties)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
var tokens = new List<AuthenticationToken>();
if (properties.Items.ContainsKey(TokenNamesKey))
{
var tokenNames = properties.Items[TokenNamesKey].Split(';');
foreach (var name in tokenNames)
{
var token = properties.GetTokenValue(name);
if (token != null)
{
tokens.Add(new AuthenticationToken { Name = name, Value = token });
}
}
}
return tokens;
}
/// <summary>
/// Extension method for getting the value of an authentication token.
/// </summary>
/// <param name="auth">The <see cref="IAuthenticationService"/>.</param>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="tokenName">The name of the token.</param>
/// <returns>The value of the token.</returns>
public static Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName)
=> auth.GetTokenAsync(context, scheme: null, tokenName: tokenName);
/// <summary>
/// Extension method for getting the value of an authentication token.
/// </summary>
/// <param name="auth">The <see cref="IAuthenticationService"/>.</param>
/// <param name="context">The <see cref="HttpContext"/> context.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="tokenName">The name of the token.</param>
/// <returns>The value of the token.</returns>
public static async Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string scheme, string tokenName)
{
if (auth == null)
{
throw new ArgumentNullException(nameof(auth));
}
if (tokenName == null)
{
throw new ArgumentNullException(nameof(tokenName));
}
var result = await auth.AuthenticateAsync(context, scheme);
return result?.Properties?.GetTokenValue(tokenName);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extension methods for setting up authentication services in an <see cref="IServiceCollection" />.
/// </summary>
public static class AuthenticationCoreServiceCollectionExtensions
{
/// <summary>
/// Add core authentication services needed for <see cref="IAuthenticationService"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <returns>The service collection.</returns>
public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAddScoped<IAuthenticationService, AuthenticationService>();
services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext
services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
return services;
}
/// <summary>
/// Add core authentication services needed for <see cref="IAuthenticationService"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="configureOptions">Used to configure the <see cref="AuthenticationOptions"/>.</param>
/// <returns>The service collection.</returns>
public static IServiceCollection AddAuthenticationCore(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) {
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.AddAuthenticationCore();
services.Configure(configureOptions);
return services;
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Used to capture path info so redirects can be computed properly within an app.Map().
/// </summary>
public class AuthenticationFeature : IAuthenticationFeature
{
/// <summary>
/// The original path base.
/// </summary>
public PathString OriginalPathBase { get; set; }
/// <summary>
/// The original path.
/// </summary>
public PathString OriginalPath { get; set; }
}
}

View File

@ -0,0 +1,63 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Implementation of <see cref="IAuthenticationHandlerProvider"/>.
/// </summary>
public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="schemes">The <see cref="IAuthenticationHandlerProvider"/>.</param>
public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
{
Schemes = schemes;
}
/// <summary>
/// The <see cref="IAuthenticationHandlerProvider"/>.
/// </summary>
public IAuthenticationSchemeProvider Schemes { get; }
// handler instance cache, need to initialize once per request
private Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal);
/// <summary>
/// Returns the handler instance that will be used.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="authenticationScheme">The name of the authentication scheme being handled.</param>
/// <returns>The handler instance.</returns>
public async Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme)
{
if (_handlerMap.ContainsKey(authenticationScheme))
{
return _handlerMap[authenticationScheme];
}
var scheme = await Schemes.GetSchemeAsync(authenticationScheme);
if (scheme == null)
{
return null;
}
var handler = (context.RequestServices.GetService(scheme.HandlerType) ??
ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType))
as IAuthenticationHandler;
if (handler != null)
{
await handler.InitializeAsync(scheme, context);
_handlerMap[authenticationScheme] = handler;
}
return handler;
}
}
}

View File

@ -0,0 +1,176 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Implements <see cref="IAuthenticationSchemeProvider"/>.
/// </summary>
public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
{
/// <summary>
/// Creates an instance of <see cref="AuthenticationSchemeProvider"/>
/// using the specified <paramref name="options"/>,
/// </summary>
/// <param name="options">The <see cref="AuthenticationOptions"/> options.</param>
public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
: this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal))
{
}
/// <summary>
/// Creates an instance of <see cref="AuthenticationSchemeProvider"/>
/// using the specified <paramref name="options"/> and <paramref name="schemes"/>.
/// </summary>
/// <param name="options">The <see cref="AuthenticationOptions"/> options.</param>
/// <param name="schemes">The dictionary used to store authentication schemes.</param>
protected AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes)
{
_options = options.Value;
_schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
_requestHandlers = new List<AuthenticationScheme>();
foreach (var builder in _options.Schemes)
{
var scheme = builder.Build();
AddScheme(scheme);
}
}
private readonly AuthenticationOptions _options;
private readonly object _lock = new object();
private readonly IDictionary<string, AuthenticationScheme> _schemes;
private readonly List<AuthenticationScheme> _requestHandlers;
private Task<AuthenticationScheme> GetDefaultSchemeAsync()
=> _options.DefaultScheme != null
? GetSchemeAsync(_options.DefaultScheme)
: Task.FromResult<AuthenticationScheme>(null);
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultAuthenticateScheme"/>.
/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.AuthenticateAsync(HttpContext, string)"/>.</returns>
public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
=> _options.DefaultAuthenticateScheme != null
? GetSchemeAsync(_options.DefaultAuthenticateScheme)
: GetDefaultSchemeAsync();
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultChallengeScheme"/>.
/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
public virtual Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
=> _options.DefaultChallengeScheme != null
? GetSchemeAsync(_options.DefaultChallengeScheme)
: GetDefaultSchemeAsync();
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultForbidScheme"/>.
/// Otherwise, this will fallback to <see cref="GetDefaultChallengeSchemeAsync"/> .
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.ForbidAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
public virtual Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
=> _options.DefaultForbidScheme != null
? GetSchemeAsync(_options.DefaultForbidScheme)
: GetDefaultChallengeSchemeAsync();
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultSignInScheme"/>.
/// Otherwise, this will fallback to <see cref="AuthenticationOptions.DefaultScheme"/>.
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignInAsync(HttpContext, string, System.Security.Claims.ClaimsPrincipal, AuthenticationProperties)"/>.</returns>
public virtual Task<AuthenticationScheme> GetDefaultSignInSchemeAsync()
=> _options.DefaultSignInScheme != null
? GetSchemeAsync(_options.DefaultSignInScheme)
: GetDefaultSchemeAsync();
/// <summary>
/// Returns the scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.
/// This is typically specified via <see cref="AuthenticationOptions.DefaultSignOutScheme"/>.
/// Otherwise this will fallback to <see cref="GetDefaultSignInSchemeAsync"/> if that supoorts sign out.
/// </summary>
/// <returns>The scheme that will be used by default for <see cref="IAuthenticationService.SignOutAsync(HttpContext, string, AuthenticationProperties)"/>.</returns>
public virtual Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync()
=> _options.DefaultSignOutScheme != null
? GetSchemeAsync(_options.DefaultSignOutScheme)
: GetDefaultSignInSchemeAsync();
/// <summary>
/// Returns the <see cref="AuthenticationScheme"/> matching the name, or null.
/// </summary>
/// <param name="name">The name of the authenticationScheme.</param>
/// <returns>The scheme or null if not found.</returns>
public virtual Task<AuthenticationScheme> GetSchemeAsync(string name)
=> Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null);
/// <summary>
/// Returns the schemes in priority order for request handling.
/// </summary>
/// <returns>The schemes in priority order for request handling</returns>
public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
=> Task.FromResult<IEnumerable<AuthenticationScheme>>(_requestHandlers);
/// <summary>
/// Registers a scheme for use by <see cref="IAuthenticationService"/>.
/// </summary>
/// <param name="scheme">The scheme.</param>
public virtual void AddScheme(AuthenticationScheme scheme)
{
if (_schemes.ContainsKey(scheme.Name))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
lock (_lock)
{
if (_schemes.ContainsKey(scheme.Name))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
{
_requestHandlers.Add(scheme);
}
_schemes[scheme.Name] = scheme;
}
}
/// <summary>
/// Removes a scheme, preventing it from being used by <see cref="IAuthenticationService"/>.
/// </summary>
/// <param name="name">The name of the authenticationScheme being removed.</param>
public virtual void RemoveScheme(string name)
{
if (!_schemes.ContainsKey(name))
{
return;
}
lock (_lock)
{
if (_schemes.ContainsKey(name))
{
var scheme = _schemes[name];
_requestHandlers.Remove(scheme);
_schemes.Remove(name);
}
}
}
public virtual Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()
=> Task.FromResult<IEnumerable<AuthenticationScheme>>(_schemes.Values);
}
}

View File

@ -0,0 +1,303 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Implements <see cref="IAuthenticationService"/>.
/// </summary>
public class AuthenticationService : IAuthenticationService
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="schemes">The <see cref="IAuthenticationSchemeProvider"/>.</param>
/// <param name="handlers">The <see cref="IAuthenticationRequestHandler"/>.</param>
/// <param name="transform">The <see cref="IClaimsTransformation"/>.</param>
public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform)
{
Schemes = schemes;
Handlers = handlers;
Transform = transform;
}
/// <summary>
/// Used to lookup AuthenticationSchemes.
/// </summary>
public IAuthenticationSchemeProvider Schemes { get; }
/// <summary>
/// Used to resolve IAuthenticationHandler instances.
/// </summary>
public IAuthenticationHandlerProvider Handlers { get; }
/// <summary>
/// Used for claims transformation.
/// </summary>
public IClaimsTransformation Transform { get; }
/// <summary>
/// Authenticate for the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <returns>The result.</returns>
public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
{
if (scheme == null)
{
var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
scheme = defaultScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found.");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
if (handler == null)
{
throw await CreateMissingHandlerException(scheme);
}
var result = await handler.AuthenticateAsync();
if (result != null && result.Succeeded)
{
var transformed = await Transform.TransformAsync(result.Principal);
return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme));
}
return result;
}
/// <summary>
/// Challenge the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
/// <returns>A task.</returns>
public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
{
if (scheme == null)
{
var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync();
scheme = defaultChallengeScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found.");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
if (handler == null)
{
throw await CreateMissingHandlerException(scheme);
}
await handler.ChallengeAsync(properties);
}
/// <summary>
/// Forbid the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
/// <returns>A task.</returns>
public virtual async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties)
{
if (scheme == null)
{
var defaultForbidScheme = await Schemes.GetDefaultForbidSchemeAsync();
scheme = defaultForbidScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultForbidScheme found.");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
if (handler == null)
{
throw await CreateMissingHandlerException(scheme);
}
await handler.ForbidAsync(properties);
}
/// <summary>
/// Sign a principal in for the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="principal">The <see cref="ClaimsPrincipal"/> to sign in.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
/// <returns>A task.</returns>
public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
if (scheme == null)
{
var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync();
scheme = defaultScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found.");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
if (handler == null)
{
throw await CreateMissingSignInHandlerException(scheme);
}
var signInHandler = handler as IAuthenticationSignInHandler;
if (signInHandler == null)
{
throw await CreateMismatchedSignInHandlerException(scheme, handler);
}
await signInHandler.SignInAsync(principal, properties);
}
/// <summary>
/// Sign out the specified authentication scheme.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="scheme">The name of the authentication scheme.</param>
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
/// <returns>A task.</returns>
public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties)
{
if (scheme == null)
{
var defaultScheme = await Schemes.GetDefaultSignOutSchemeAsync();
scheme = defaultScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found.");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
if (handler == null)
{
throw await CreateMissingSignOutHandlerException(scheme);
}
var signOutHandler = handler as IAuthenticationSignOutHandler;
if (signOutHandler == null)
{
throw await CreateMismatchedSignOutHandlerException(scheme, handler);
}
await signOutHandler.SignOutAsync(properties);
}
private async Task<Exception> CreateMissingHandlerException(string scheme)
{
var schemes = string.Join(", ", (await Schemes.GetAllSchemesAsync()).Select(sch => sch.Name));
var footer = $" Did you forget to call AddAuthentication().Add[SomeAuthHandler](\"{scheme}\",...)?";
if (string.IsNullOrEmpty(schemes))
{
return new InvalidOperationException(
$"No authentication handlers are registered." + footer);
}
return new InvalidOperationException(
$"No authentication handler is registered for the scheme '{scheme}'. The registered schemes are: {schemes}." + footer);
}
private async Task<string> GetAllSignInSchemeNames()
{
return string.Join(", ", (await Schemes.GetAllSchemesAsync())
.Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType))
.Select(sch => sch.Name));
}
private async Task<Exception> CreateMissingSignInHandlerException(string scheme)
{
var schemes = await GetAllSignInSchemeNames();
// CookieAuth is the only implementation of sign-in.
var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?";
if (string.IsNullOrEmpty(schemes))
{
return new InvalidOperationException(
$"No sign-in authentication handlers are registered." + footer);
}
return new InvalidOperationException(
$"No sign-in authentication handler is registered for the scheme '{scheme}'. The registered sign-in schemes are: {schemes}." + footer);
}
private async Task<Exception> CreateMismatchedSignInHandlerException(string scheme, IAuthenticationHandler handler)
{
var schemes = await GetAllSignInSchemeNames();
var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for SignInAsync. ";
if (string.IsNullOrEmpty(schemes))
{
// CookieAuth is the only implementation of sign-in.
return new InvalidOperationException(mismatchError
+ $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?");
}
return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}.");
}
private async Task<string> GetAllSignOutSchemeNames()
{
return string.Join(", ", (await Schemes.GetAllSchemesAsync())
.Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType))
.Select(sch => sch.Name));
}
private async Task<Exception> CreateMissingSignOutHandlerException(string scheme)
{
var schemes = await GetAllSignOutSchemeNames();
var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?";
if (string.IsNullOrEmpty(schemes))
{
// CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it.
return new InvalidOperationException($"No sign-out authentication handlers are registered." + footer);
}
return new InvalidOperationException(
$"No sign-out authentication handler is registered for the scheme '{scheme}'. The registered sign-out schemes are: {schemes}." + footer);
}
private async Task<Exception> CreateMismatchedSignOutHandlerException(string scheme, IAuthenticationHandler handler)
{
var schemes = await GetAllSignOutSchemeNames();
var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for {nameof(SignOutAsync)}. ";
if (string.IsNullOrEmpty(schemes))
{
// CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it.
return new InvalidOperationException(mismatchError
+ $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?");
}
return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}.");
}
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core common types used by the various authentication middleware components.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;authentication;security</PackageTags>
<EnableApiCheck>false</EnableApiCheck>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authentication.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Claims;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Authentication
{
/// <summary>
/// Default claims transformation is a no-op.
/// </summary>
public class NoopClaimsTransformation : IClaimsTransformation
{
/// <summary>
/// Returns the principal unchanged.
/// </summary>
/// <param name="principal">The user.</param>
/// <returns>The principal unchanged.</returns>
public virtual Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
return Task.FromResult(principal);
}
}
}

View File

@ -0,0 +1,515 @@
{
"AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
"Types": [
{
"Name": "Microsoft.AspNetCore.Authentication.AuthenticationFeature",
"Visibility": "Public",
"Kind": "Class",
"ImplementedInterfaces": [
"Microsoft.AspNetCore.Authentication.IAuthenticationFeature"
],
"Members": [
{
"Kind": "Method",
"Name": "get_OriginalPathBase",
"Parameters": [],
"ReturnType": "Microsoft.AspNetCore.Http.PathString",
"Sealed": true,
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_OriginalPathBase",
"Parameters": [
{
"Name": "value",
"Type": "Microsoft.AspNetCore.Http.PathString"
}
],
"ReturnType": "System.Void",
"Sealed": true,
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_OriginalPath",
"Parameters": [],
"ReturnType": "Microsoft.AspNetCore.Http.PathString",
"Sealed": true,
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_OriginalPath",
"Parameters": [
{
"Name": "value",
"Type": "Microsoft.AspNetCore.Http.PathString"
}
],
"ReturnType": "System.Void",
"Sealed": true,
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Constructor",
"Name": ".ctor",
"Parameters": [],
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
},
{
"Name": "Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider",
"Visibility": "Public",
"Kind": "Class",
"ImplementedInterfaces": [
"Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider"
],
"Members": [
{
"Kind": "Method",
"Name": "get_Schemes",
"Parameters": [],
"ReturnType": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "GetHandlerAsync",
"Parameters": [
{
"Name": "context",
"Type": "Microsoft.AspNetCore.Http.HttpContext"
},
{
"Name": "authenticationScheme",
"Type": "System.String"
}
],
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.IAuthenticationHandler>",
"Sealed": true,
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Constructor",
"Name": ".ctor",
"Parameters": [
{
"Name": "schemes",
"Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider"
}
],
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
},
{
"Name": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider",
"Visibility": "Public",
"Kind": "Class",
"ImplementedInterfaces": [
"Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider"
],
"Members": [
{
"Kind": "Method",
"Name": "GetDefaultAuthenticateSchemeAsync",
"Parameters": [],
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "GetDefaultChallengeSchemeAsync",
"Parameters": [],
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "GetDefaultForbidSchemeAsync",
"Parameters": [],
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "GetDefaultSignInSchemeAsync",
"Parameters": [],
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "GetDefaultSignOutSchemeAsync",
"Parameters": [],
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "GetSchemeAsync",
"Parameters": [
{
"Name": "name",
"Type": "System.String"
}
],
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationScheme>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "GetRequestHandlerSchemesAsync",
"Parameters": [],
"ReturnType": "System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.AuthenticationScheme>>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "AddScheme",
"Parameters": [
{
"Name": "scheme",
"Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
}
],
"ReturnType": "System.Void",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "RemoveScheme",
"Parameters": [
{
"Name": "name",
"Type": "System.String"
}
],
"ReturnType": "System.Void",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "GetAllSchemesAsync",
"Parameters": [],
"ReturnType": "System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.AuthenticationScheme>>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Constructor",
"Name": ".ctor",
"Parameters": [
{
"Name": "options",
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Authentication.AuthenticationOptions>"
}
],
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
},
{
"Name": "Microsoft.AspNetCore.Authentication.AuthenticationService",
"Visibility": "Public",
"Kind": "Class",
"ImplementedInterfaces": [
"Microsoft.AspNetCore.Authentication.IAuthenticationService"
],
"Members": [
{
"Kind": "Method",
"Name": "get_Schemes",
"Parameters": [],
"ReturnType": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_Handlers",
"Parameters": [],
"ReturnType": "Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "get_Transform",
"Parameters": [],
"ReturnType": "Microsoft.AspNetCore.Authentication.IClaimsTransformation",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "AuthenticateAsync",
"Parameters": [
{
"Name": "context",
"Type": "Microsoft.AspNetCore.Http.HttpContext"
},
{
"Name": "scheme",
"Type": "System.String"
}
],
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "ChallengeAsync",
"Parameters": [
{
"Name": "context",
"Type": "Microsoft.AspNetCore.Http.HttpContext"
},
{
"Name": "scheme",
"Type": "System.String"
},
{
"Name": "properties",
"Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
}
],
"ReturnType": "System.Threading.Tasks.Task",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "ForbidAsync",
"Parameters": [
{
"Name": "context",
"Type": "Microsoft.AspNetCore.Http.HttpContext"
},
{
"Name": "scheme",
"Type": "System.String"
},
{
"Name": "properties",
"Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
}
],
"ReturnType": "System.Threading.Tasks.Task",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "SignInAsync",
"Parameters": [
{
"Name": "context",
"Type": "Microsoft.AspNetCore.Http.HttpContext"
},
{
"Name": "scheme",
"Type": "System.String"
},
{
"Name": "principal",
"Type": "System.Security.Claims.ClaimsPrincipal"
},
{
"Name": "properties",
"Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
}
],
"ReturnType": "System.Threading.Tasks.Task",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "SignOutAsync",
"Parameters": [
{
"Name": "context",
"Type": "Microsoft.AspNetCore.Http.HttpContext"
},
{
"Name": "scheme",
"Type": "System.String"
},
{
"Name": "properties",
"Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
}
],
"ReturnType": "System.Threading.Tasks.Task",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationService",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Constructor",
"Name": ".ctor",
"Parameters": [
{
"Name": "schemes",
"Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider"
},
{
"Name": "handlers",
"Type": "Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider"
},
{
"Name": "transform",
"Type": "Microsoft.AspNetCore.Authentication.IClaimsTransformation"
}
],
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
},
{
"Name": "Microsoft.AspNetCore.Authentication.NoopClaimsTransformation",
"Visibility": "Public",
"Kind": "Class",
"ImplementedInterfaces": [
"Microsoft.AspNetCore.Authentication.IClaimsTransformation"
],
"Members": [
{
"Kind": "Method",
"Name": "TransformAsync",
"Parameters": [
{
"Name": "principal",
"Type": "System.Security.Claims.ClaimsPrincipal"
}
],
"ReturnType": "System.Threading.Tasks.Task<System.Security.Claims.ClaimsPrincipal>",
"Virtual": true,
"ImplementedInterface": "Microsoft.AspNetCore.Authentication.IClaimsTransformation",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Constructor",
"Name": ".ctor",
"Parameters": [],
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
},
{
"Name": "Microsoft.Extensions.DependencyInjection.AuthenticationCoreServiceCollectionExtensions",
"Visibility": "Public",
"Kind": "Class",
"Abstract": true,
"Static": true,
"Sealed": true,
"ImplementedInterfaces": [],
"Members": [
{
"Kind": "Method",
"Name": "AddAuthenticationCore",
"Parameters": [
{
"Name": "services",
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
}
],
"ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
"Static": true,
"Extension": true,
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "AddAuthenticationCore",
"Parameters": [
{
"Name": "services",
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
},
{
"Name": "configureOptions",
"Type": "System.Action<Microsoft.AspNetCore.Authentication.AuthenticationOptions>"
}
],
"ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
"Static": true,
"Extension": true,
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
}
]
}

View File

@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Authentication.Core.Test
{
public class AuthenticationPropertiesTests
{
[Fact]
public void DefaultConstructor_EmptyCollections()
{
var props = new AuthenticationProperties();
Assert.Empty(props.Items);
Assert.Empty(props.Parameters);
}
[Fact]
public void ItemsConstructor_ReusesItemsDictionary()
{
var items = new Dictionary<string, string>
{
["foo"] = "bar",
};
var props = new AuthenticationProperties(items);
Assert.Same(items, props.Items);
Assert.Empty(props.Parameters);
}
[Fact]
public void FullConstructor_ReusesDictionaries()
{
var items = new Dictionary<string, string>
{
["foo"] = "bar",
};
var parameters = new Dictionary<string, object>
{
["number"] = 1234,
["list"] = new List<string> { "a", "b", "c" },
};
var props = new AuthenticationProperties(items, parameters);
Assert.Same(items, props.Items);
Assert.Same(parameters, props.Parameters);
}
[Fact]
public void GetSetString()
{
var props = new AuthenticationProperties();
Assert.Null(props.GetString("foo"));
Assert.Equal(0, props.Items.Count);
props.SetString("foo", "foo bar");
Assert.Equal("foo bar", props.GetString("foo"));
Assert.Equal("foo bar", props.Items["foo"]);
Assert.Equal(1, props.Items.Count);
props.SetString("foo", "foo baz");
Assert.Equal("foo baz", props.GetString("foo"));
Assert.Equal("foo baz", props.Items["foo"]);
Assert.Equal(1, props.Items.Count);
props.SetString("bar", "xy");
Assert.Equal("xy", props.GetString("bar"));
Assert.Equal("xy", props.Items["bar"]);
Assert.Equal(2, props.Items.Count);
props.SetString("bar", string.Empty);
Assert.Equal(string.Empty, props.GetString("bar"));
Assert.Equal(string.Empty, props.Items["bar"]);
props.SetString("foo", null);
Assert.Null(props.GetString("foo"));
Assert.Equal(1, props.Items.Count);
}
[Fact]
public void GetSetParameter_String()
{
var props = new AuthenticationProperties();
Assert.Null(props.GetParameter<string>("foo"));
Assert.Equal(0, props.Parameters.Count);
props.SetParameter<string>("foo", "foo bar");
Assert.Equal("foo bar", props.GetParameter<string>("foo"));
Assert.Equal("foo bar", props.Parameters["foo"]);
Assert.Equal(1, props.Parameters.Count);
props.SetParameter<string>("foo", null);
Assert.Null(props.GetParameter<string>("foo"));
Assert.Null(props.Parameters["foo"]);
Assert.Equal(1, props.Parameters.Count);
}
[Fact]
public void GetSetParameter_Int()
{
var props = new AuthenticationProperties();
Assert.Null(props.GetParameter<int?>("foo"));
Assert.Equal(0, props.Parameters.Count);
props.SetParameter<int?>("foo", 123);
Assert.Equal(123, props.GetParameter<int?>("foo"));
Assert.Equal(123, props.Parameters["foo"]);
Assert.Equal(1, props.Parameters.Count);
props.SetParameter<int?>("foo", null);
Assert.Null(props.GetParameter<int?>("foo"));
Assert.Null(props.Parameters["foo"]);
Assert.Equal(1, props.Parameters.Count);
}
[Fact]
public void GetSetParameter_Collection()
{
var props = new AuthenticationProperties();
Assert.Null(props.GetParameter<int?>("foo"));
Assert.Equal(0, props.Parameters.Count);
var list = new string[] { "a", "b", "c" };
props.SetParameter<ICollection<string>>("foo", list);
Assert.Equal(new string[] { "a", "b", "c" }, props.GetParameter<ICollection<string>>("foo"));
Assert.Same(list, props.Parameters["foo"]);
Assert.Equal(1, props.Parameters.Count);
props.SetParameter<ICollection<string>>("foo", null);
Assert.Null(props.GetParameter<ICollection<string>>("foo"));
Assert.Null(props.Parameters["foo"]);
Assert.Equal(1, props.Parameters.Count);
}
[Fact]
public void IsPersistent_Test()
{
var props = new AuthenticationProperties();
Assert.False(props.IsPersistent);
props.IsPersistent = true;
Assert.True(props.IsPersistent);
Assert.Equal(string.Empty, props.Items.First().Value);
props.Items.Clear();
Assert.False(props.IsPersistent);
}
[Fact]
public void RedirectUri_Test()
{
var props = new AuthenticationProperties();
Assert.Null(props.RedirectUri);
props.RedirectUri = "http://example.com";
Assert.Equal("http://example.com", props.RedirectUri);
Assert.Equal("http://example.com", props.Items.First().Value);
props.Items.Clear();
Assert.Null(props.RedirectUri);
}
[Fact]
public void IssuedUtc_Test()
{
var props = new AuthenticationProperties();
Assert.Null(props.IssuedUtc);
props.IssuedUtc = new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc));
Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc)), props.IssuedUtc);
Assert.Equal("Wed, 21 Mar 2018 00:00:00 GMT", props.Items.First().Value);
props.Items.Clear();
Assert.Null(props.IssuedUtc);
}
[Fact]
public void ExpiresUtc_Test()
{
var props = new AuthenticationProperties();
Assert.Null(props.ExpiresUtc);
props.ExpiresUtc = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc));
Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)), props.ExpiresUtc);
Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items.First().Value);
props.Items.Clear();
Assert.Null(props.ExpiresUtc);
}
[Fact]
public void AllowRefresh_Test()
{
var props = new AuthenticationProperties();
Assert.Null(props.AllowRefresh);
props.AllowRefresh = true;
Assert.True(props.AllowRefresh);
Assert.Equal("True", props.Items.First().Value);
props.AllowRefresh = false;
Assert.False(props.AllowRefresh);
Assert.Equal("False", props.Items.First().Value);
props.Items.Clear();
Assert.Null(props.AllowRefresh);
}
}
}

View File

@ -0,0 +1,207 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Authentication
{
public class AuthenticationSchemeProviderTests
{
[Fact]
public async Task NoDefaultsByDefault()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<SignInHandler>("B", "whatever");
}).BuildServiceProvider();
var provider = services.GetRequiredService<IAuthenticationSchemeProvider>();
Assert.Null(await provider.GetDefaultForbidSchemeAsync());
Assert.Null(await provider.GetDefaultAuthenticateSchemeAsync());
Assert.Null(await provider.GetDefaultChallengeSchemeAsync());
Assert.Null(await provider.GetDefaultSignInSchemeAsync());
Assert.Null(await provider.GetDefaultSignOutSchemeAsync());
}
[Fact]
public async Task DefaultSchemesFallbackToDefaultScheme()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.DefaultScheme = "B";
o.AddScheme<SignInHandler>("B", "whatever");
}).BuildServiceProvider();
var provider = services.GetRequiredService<IAuthenticationSchemeProvider>();
Assert.Equal("B", (await provider.GetDefaultForbidSchemeAsync()).Name);
Assert.Equal("B", (await provider.GetDefaultAuthenticateSchemeAsync()).Name);
Assert.Equal("B", (await provider.GetDefaultChallengeSchemeAsync()).Name);
Assert.Equal("B", (await provider.GetDefaultSignInSchemeAsync()).Name);
Assert.Equal("B", (await provider.GetDefaultSignOutSchemeAsync()).Name);
}
[Fact]
public async Task DefaultSignOutFallsbackToSignIn()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<SignInHandler>("signin", "whatever");
o.AddScheme<Handler>("foobly", "whatever");
o.DefaultSignInScheme = "signin";
}).BuildServiceProvider();
var provider = services.GetRequiredService<IAuthenticationSchemeProvider>();
var scheme = await provider.GetDefaultSignOutSchemeAsync();
Assert.NotNull(scheme);
Assert.Equal("signin", scheme.Name);
}
[Fact]
public async Task DefaultForbidFallsbackToChallenge()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<Handler>("challenge", "whatever");
o.AddScheme<Handler>("foobly", "whatever");
o.DefaultChallengeScheme = "challenge";
}).BuildServiceProvider();
var provider = services.GetRequiredService<IAuthenticationSchemeProvider>();
var scheme = await provider.GetDefaultForbidSchemeAsync();
Assert.NotNull(scheme);
Assert.Equal("challenge", scheme.Name);
}
[Fact]
public async Task DefaultSchemesAreSet()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<SignInHandler>("A", "whatever");
o.AddScheme<SignInHandler>("B", "whatever");
o.AddScheme<SignInHandler>("C", "whatever");
o.AddScheme<SignInHandler>("Def", "whatever");
o.DefaultScheme = "Def";
o.DefaultChallengeScheme = "A";
o.DefaultForbidScheme = "B";
o.DefaultSignInScheme = "C";
o.DefaultSignOutScheme = "A";
o.DefaultAuthenticateScheme = "C";
}).BuildServiceProvider();
var provider = services.GetRequiredService<IAuthenticationSchemeProvider>();
Assert.Equal("B", (await provider.GetDefaultForbidSchemeAsync()).Name);
Assert.Equal("C", (await provider.GetDefaultAuthenticateSchemeAsync()).Name);
Assert.Equal("A", (await provider.GetDefaultChallengeSchemeAsync()).Name);
Assert.Equal("C", (await provider.GetDefaultSignInSchemeAsync()).Name);
Assert.Equal("A", (await provider.GetDefaultSignOutSchemeAsync()).Name);
}
[Fact]
public async Task SignOutWillDefaultsToSignInThatDoesNotSignOut()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<Handler>("signin", "whatever");
o.DefaultSignInScheme = "signin";
}).BuildServiceProvider();
var provider = services.GetRequiredService<IAuthenticationSchemeProvider>();
Assert.NotNull(await provider.GetDefaultSignOutSchemeAsync());
}
[Fact]
public void SchemeRegistrationIsCaseSensitive()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<Handler>("signin", "whatever");
o.AddScheme<Handler>("signin", "whatever");
}).BuildServiceProvider();
var error = Assert.Throws<InvalidOperationException>(() => services.GetRequiredService<IAuthenticationSchemeProvider>());
Assert.Contains("Scheme already exists: signin", error.Message);
}
[Fact]
public async Task LookupUsesProvidedStringComparer()
{
var services = new ServiceCollection().AddOptions()
.AddSingleton<IAuthenticationSchemeProvider, IgnoreCaseSchemeProvider>()
.AddAuthenticationCore(o => o.AddScheme<Handler>("signin", "whatever"))
.BuildServiceProvider();
var provider = services.GetRequiredService<IAuthenticationSchemeProvider>();
var a = await provider.GetSchemeAsync("signin");
var b = await provider.GetSchemeAsync("SignIn");
var c = await provider.GetSchemeAsync("SIGNIN");
Assert.NotNull(a);
Assert.Same(a, b);
Assert.Same(b, c);
}
private class Handler : IAuthenticationHandler
{
public Task<AuthenticateResult> AuthenticateAsync()
{
throw new NotImplementedException();
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task ForbidAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
throw new NotImplementedException();
}
}
private class SignInHandler : Handler, IAuthenticationSignInHandler
{
public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task SignOutAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
}
private class SignOutHandler : Handler, IAuthenticationSignOutHandler
{
public Task SignOutAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
}
private class IgnoreCaseSchemeProvider : AuthenticationSchemeProvider
{
public IgnoreCaseSchemeProvider(IOptions<AuthenticationOptions> options)
: base(options, new Dictionary<string, AuthenticationScheme>(StringComparer.OrdinalIgnoreCase))
{
}
}
}
}

View File

@ -0,0 +1,355 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Authentication
{
public class AuthenticationServiceTests
{
[Fact]
public async Task AuthenticateThrowsForSchemeMismatch()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<BaseHandler>("base", "whatever");
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.AuthenticateAsync("base");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.AuthenticateAsync("missing"));
Assert.Contains("base", ex.Message);
}
[Fact]
public async Task ChallengeThrowsForSchemeMismatch()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<BaseHandler>("base", "whatever");
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.ChallengeAsync("base");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.ChallengeAsync("missing"));
Assert.Contains("base", ex.Message);
}
[Fact]
public async Task ForbidThrowsForSchemeMismatch()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<BaseHandler>("base", "whatever");
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.ForbidAsync("base");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.ForbidAsync("missing"));
Assert.Contains("base", ex.Message);
}
[Fact]
public async Task CanOnlySignInIfSupported()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<UberHandler>("uber", "whatever");
o.AddScheme<BaseHandler>("base", "whatever");
o.AddScheme<SignInHandler>("signin", "whatever");
o.AddScheme<SignOutHandler>("signout", "whatever");
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.SignInAsync("uber", new ClaimsPrincipal(), null);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("base", new ClaimsPrincipal(), null));
Assert.Contains("uber", ex.Message);
Assert.Contains("signin", ex.Message);
await context.SignInAsync("signin", new ClaimsPrincipal(), null);
ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("signout", new ClaimsPrincipal(), null));
Assert.Contains("uber", ex.Message);
Assert.Contains("signin", ex.Message);
}
[Fact]
public async Task CanOnlySignOutIfSupported()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<UberHandler>("uber", "whatever");
o.AddScheme<BaseHandler>("base", "whatever");
o.AddScheme<SignInHandler>("signin", "whatever");
o.AddScheme<SignOutHandler>("signout", "whatever");
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.SignOutAsync("uber");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync("base"));
Assert.Contains("uber", ex.Message);
Assert.Contains("signout", ex.Message);
await context.SignOutAsync("signout");
await context.SignOutAsync("signin");
}
[Fact]
public async Task ServicesWithDefaultIAuthenticationHandlerMethodsTest()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<BaseHandler>("base", "whatever");
o.DefaultScheme = "base";
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.AuthenticateAsync();
await context.ChallengeAsync();
await context.ForbidAsync();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
Assert.Contains("cannot be used for SignOutAsync", ex.Message);
ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
Assert.Contains("cannot be used for SignInAsync", ex.Message);
}
[Fact]
public async Task ServicesWithDefaultUberMethodsTest()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<UberHandler>("base", "whatever");
o.DefaultScheme = "base";
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.AuthenticateAsync();
await context.ChallengeAsync();
await context.ForbidAsync();
await context.SignOutAsync();
await context.SignInAsync(new ClaimsPrincipal());
}
[Fact]
public async Task ServicesWithDefaultSignInMethodsTest()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<SignInHandler>("base", "whatever");
o.DefaultScheme = "base";
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.AuthenticateAsync();
await context.ChallengeAsync();
await context.ForbidAsync();
await context.SignOutAsync();
await context.SignInAsync(new ClaimsPrincipal());
}
[Fact]
public async Task ServicesWithDefaultSignOutMethodsTest()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<SignOutHandler>("base", "whatever");
o.DefaultScheme = "base";
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.AuthenticateAsync();
await context.ChallengeAsync();
await context.ForbidAsync();
await context.SignOutAsync();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
Assert.Contains("cannot be used for SignInAsync", ex.Message);
}
[Fact]
public async Task ServicesWithDefaultForbidMethod_CallsForbidMethod()
{
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
{
o.AddScheme<ForbidHandler>("forbid", "whatever");
o.DefaultForbidScheme = "forbid";
}).BuildServiceProvider();
var context = new DefaultHttpContext();
context.RequestServices = services;
await context.ForbidAsync();
}
private class BaseHandler : IAuthenticationHandler
{
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
return Task.FromResult(0);
}
}
private class SignInHandler : IAuthenticationSignInHandler
{
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
return Task.FromResult(0);
}
public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task SignOutAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
}
public class SignOutHandler : IAuthenticationSignOutHandler
{
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
return Task.FromResult(0);
}
public Task SignOutAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
}
private class UberHandler : IAuthenticationHandler, IAuthenticationRequestHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler
{
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task<bool> HandleRequestAsync()
{
return Task.FromResult(false);
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
return Task.FromResult(0);
}
public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task SignOutAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
}
private class ForbidHandler : IAuthenticationHandler, IAuthenticationRequestHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler
{
public Task<AuthenticateResult> AuthenticateAsync()
{
throw new NotImplementedException();
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.FromResult(0);
}
public Task<bool> HandleRequestAsync()
{
throw new NotImplementedException();
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
return Task.FromResult(0);
}
public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task SignOutAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authentication.Core" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,200 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Authentication
{
public class TokenExtensionTests
{
[Fact]
public void CanStoreMultipleTokens()
{
var props = new AuthenticationProperties();
var tokens = new List<AuthenticationToken>();
var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
tokens.Add(tok1);
tokens.Add(tok2);
tokens.Add(tok3);
props.StoreTokens(tokens);
Assert.Equal("1", props.GetTokenValue("One"));
Assert.Equal("2", props.GetTokenValue("Two"));
Assert.Equal("3", props.GetTokenValue("Three"));
Assert.Equal(3, props.GetTokens().Count());
}
[Fact]
public void SubsequentStoreTokenDeletesPreviousTokens()
{
var props = new AuthenticationProperties();
var tokens = new List<AuthenticationToken>();
var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
tokens.Add(tok1);
tokens.Add(tok2);
tokens.Add(tok3);
props.StoreTokens(tokens);
props.StoreTokens(new[] { new AuthenticationToken { Name = "Zero", Value = "0" } });
Assert.Equal("0", props.GetTokenValue("Zero"));
Assert.Null(props.GetTokenValue("One"));
Assert.Null(props.GetTokenValue("Two"));
Assert.Null(props.GetTokenValue("Three"));
Assert.Single(props.GetTokens());
}
[Fact]
public void CanUpdateTokens()
{
var props = new AuthenticationProperties();
var tokens = new List<AuthenticationToken>();
var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
tokens.Add(tok1);
tokens.Add(tok2);
tokens.Add(tok3);
props.StoreTokens(tokens);
tok1.Value = ".1";
tok2.Value = ".2";
tok3.Value = ".3";
props.StoreTokens(tokens);
Assert.Equal(".1", props.GetTokenValue("One"));
Assert.Equal(".2", props.GetTokenValue("Two"));
Assert.Equal(".3", props.GetTokenValue("Three"));
Assert.Equal(3, props.GetTokens().Count());
}
[Fact]
public void CanUpdateTokenValues()
{
var props = new AuthenticationProperties();
var tokens = new List<AuthenticationToken>();
var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
tokens.Add(tok1);
tokens.Add(tok2);
tokens.Add(tok3);
props.StoreTokens(tokens);
Assert.True(props.UpdateTokenValue("One", ".11"));
Assert.True(props.UpdateTokenValue("Two", ".22"));
Assert.True(props.UpdateTokenValue("Three", ".33"));
Assert.Equal(".11", props.GetTokenValue("One"));
Assert.Equal(".22", props.GetTokenValue("Two"));
Assert.Equal(".33", props.GetTokenValue("Three"));
Assert.Equal(3, props.GetTokens().Count());
}
[Fact]
public void UpdateTokenValueReturnsFalseForUnknownToken()
{
var props = new AuthenticationProperties();
var tokens = new List<AuthenticationToken>();
var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
tokens.Add(tok1);
tokens.Add(tok2);
tokens.Add(tok3);
props.StoreTokens(tokens);
Assert.False(props.UpdateTokenValue("ONE", ".11"));
Assert.False(props.UpdateTokenValue("Jigglypuff", ".11"));
Assert.Null(props.GetTokenValue("ONE"));
Assert.Null(props.GetTokenValue("Jigglypuff"));
Assert.Equal(3, props.GetTokens().Count());
}
[Fact]
public async Task GetTokenWorksWithDefaultAuthenticateScheme()
{
var context = new DefaultHttpContext();
var services = new ServiceCollection().AddOptions()
.AddAuthenticationCore(o =>
{
o.DefaultScheme = "simple";
o.AddScheme("simple", s => s.HandlerType = typeof(SimpleAuth));
});
context.RequestServices = services.BuildServiceProvider();
Assert.Equal("1", await context.GetTokenAsync("One"));
Assert.Equal("2", await context.GetTokenAsync("Two"));
Assert.Equal("3", await context.GetTokenAsync("Three"));
}
[Fact]
public async Task GetTokenWorksWithExplicitScheme()
{
var context = new DefaultHttpContext();
var services = new ServiceCollection().AddOptions()
.AddAuthenticationCore(o => o.AddScheme("simple", s => s.HandlerType = typeof(SimpleAuth)));
context.RequestServices = services.BuildServiceProvider();
Assert.Equal("1", await context.GetTokenAsync("simple", "One"));
Assert.Equal("2", await context.GetTokenAsync("simple", "Two"));
Assert.Equal("3", await context.GetTokenAsync("simple", "Three"));
}
private class SimpleAuth : IAuthenticationHandler
{
public Task<AuthenticateResult> AuthenticateAsync()
{
var props = new AuthenticationProperties();
var tokens = new List<AuthenticationToken>();
var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
tokens.Add(tok1);
tokens.Add(tok2);
tokens.Add(tok3);
props.StoreTokens(tokens);
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), props, "simple")));
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task ForbidAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
return Task.FromResult(0);
}
public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task SignOutAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
internal abstract class BaseHeaderParser<T> : HttpHeaderParser<T>
{
protected BaseHeaderParser(bool supportsMultipleValues)
: base(supportsMultipleValues)
{
}
protected abstract int GetParsedValueLength(StringSegment value, int startIndex, out T parsedValue);
public sealed override bool TryParseValue(StringSegment value, ref int index, out T parsedValue)
{
parsedValue = default(T);
// If multiple values are supported (i.e. list of values), then accept an empty string: The header may
// be added multiple times to the request/response message. E.g.
// Accept: text/xml; q=1
// Accept:
// Accept: text/plain; q=0.2
if (StringSegment.IsNullOrEmpty(value) || (index == value.Length))
{
return SupportsMultipleValues;
}
var separatorFound = false;
var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues,
out separatorFound);
if (separatorFound && !SupportsMultipleValues)
{
return false; // leading separators not allowed if we don't support multiple values.
}
if (current == value.Length)
{
if (SupportsMultipleValues)
{
index = current;
}
return SupportsMultipleValues;
}
T result;
var length = GetParsedValueLength(value, current, out result);
if (length == 0)
{
return false;
}
current = current + length;
current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues,
out separatorFound);
// If we support multiple values and we've not reached the end of the string, then we must have a separator.
if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length)))
{
return false;
}
index = current;
parsedValue = result;
return true;
}
}
}

View File

@ -0,0 +1,664 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
public class CacheControlHeaderValue
{
public static readonly string PublicString = "public";
public static readonly string PrivateString = "private";
public static readonly string MaxAgeString = "max-age";
public static readonly string SharedMaxAgeString = "s-maxage";
public static readonly string NoCacheString = "no-cache";
public static readonly string NoStoreString = "no-store";
public static readonly string MaxStaleString = "max-stale";
public static readonly string MinFreshString = "min-fresh";
public static readonly string NoTransformString = "no-transform";
public static readonly string OnlyIfCachedString = "only-if-cached";
public static readonly string MustRevalidateString = "must-revalidate";
public static readonly string ProxyRevalidateString = "proxy-revalidate";
// The Cache-Control header is special: It is a header supporting a list of values, but we represent the list
// as _one_ instance of CacheControlHeaderValue. I.e we set 'SupportsMultipleValues' to 'true' since it is
// OK to have multiple Cache-Control headers in a request/response message. However, after parsing all
// Cache-Control headers, only one instance of CacheControlHeaderValue is created (if all headers contain valid
// values, otherwise we may have multiple strings containing the invalid values).
private static readonly HttpHeaderParser<CacheControlHeaderValue> Parser
= new GenericHeaderParser<CacheControlHeaderValue>(true, GetCacheControlLength);
private static readonly Action<StringSegment> CheckIsValidTokenAction = CheckIsValidToken;
private bool _noCache;
private ICollection<StringSegment> _noCacheHeaders;
private bool _noStore;
private TimeSpan? _maxAge;
private TimeSpan? _sharedMaxAge;
private bool _maxStale;
private TimeSpan? _maxStaleLimit;
private TimeSpan? _minFresh;
private bool _noTransform;
private bool _onlyIfCached;
private bool _public;
private bool _private;
private ICollection<StringSegment> _privateHeaders;
private bool _mustRevalidate;
private bool _proxyRevalidate;
private IList<NameValueHeaderValue> _extensions;
public CacheControlHeaderValue()
{
// This type is unique in that there is no single required parameter.
}
public bool NoCache
{
get { return _noCache; }
set { _noCache = value; }
}
public ICollection<StringSegment> NoCacheHeaders
{
get
{
if (_noCacheHeaders == null)
{
_noCacheHeaders = new ObjectCollection<StringSegment>(CheckIsValidTokenAction);
}
return _noCacheHeaders;
}
}
public bool NoStore
{
get { return _noStore; }
set { _noStore = value; }
}
public TimeSpan? MaxAge
{
get { return _maxAge; }
set { _maxAge = value; }
}
public TimeSpan? SharedMaxAge
{
get { return _sharedMaxAge; }
set { _sharedMaxAge = value; }
}
public bool MaxStale
{
get { return _maxStale; }
set { _maxStale = value; }
}
public TimeSpan? MaxStaleLimit
{
get { return _maxStaleLimit; }
set { _maxStaleLimit = value; }
}
public TimeSpan? MinFresh
{
get { return _minFresh; }
set { _minFresh = value; }
}
public bool NoTransform
{
get { return _noTransform; }
set { _noTransform = value; }
}
public bool OnlyIfCached
{
get { return _onlyIfCached; }
set { _onlyIfCached = value; }
}
public bool Public
{
get { return _public; }
set { _public = value; }
}
public bool Private
{
get { return _private; }
set { _private = value; }
}
public ICollection<StringSegment> PrivateHeaders
{
get
{
if (_privateHeaders == null)
{
_privateHeaders = new ObjectCollection<StringSegment>(CheckIsValidTokenAction);
}
return _privateHeaders;
}
}
public bool MustRevalidate
{
get { return _mustRevalidate; }
set { _mustRevalidate = value; }
}
public bool ProxyRevalidate
{
get { return _proxyRevalidate; }
set { _proxyRevalidate = value; }
}
public IList<NameValueHeaderValue> Extensions
{
get
{
if (_extensions == null)
{
_extensions = new ObjectCollection<NameValueHeaderValue>();
}
return _extensions;
}
}
public override string ToString()
{
var sb = new StringBuilder();
AppendValueIfRequired(sb, _noStore, NoStoreString);
AppendValueIfRequired(sb, _noTransform, NoTransformString);
AppendValueIfRequired(sb, _onlyIfCached, OnlyIfCachedString);
AppendValueIfRequired(sb, _public, PublicString);
AppendValueIfRequired(sb, _mustRevalidate, MustRevalidateString);
AppendValueIfRequired(sb, _proxyRevalidate, ProxyRevalidateString);
if (_noCache)
{
AppendValueWithSeparatorIfRequired(sb, NoCacheString);
if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0))
{
sb.Append("=\"");
AppendValues(sb, _noCacheHeaders);
sb.Append('\"');
}
}
if (_maxAge.HasValue)
{
AppendValueWithSeparatorIfRequired(sb, MaxAgeString);
sb.Append('=');
sb.Append(((int)_maxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
}
if (_sharedMaxAge.HasValue)
{
AppendValueWithSeparatorIfRequired(sb, SharedMaxAgeString);
sb.Append('=');
sb.Append(((int)_sharedMaxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
}
if (_maxStale)
{
AppendValueWithSeparatorIfRequired(sb, MaxStaleString);
if (_maxStaleLimit.HasValue)
{
sb.Append('=');
sb.Append(((int)_maxStaleLimit.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
}
}
if (_minFresh.HasValue)
{
AppendValueWithSeparatorIfRequired(sb, MinFreshString);
sb.Append('=');
sb.Append(((int)_minFresh.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
}
if (_private)
{
AppendValueWithSeparatorIfRequired(sb, PrivateString);
if ((_privateHeaders != null) && (_privateHeaders.Count > 0))
{
sb.Append("=\"");
AppendValues(sb, _privateHeaders);
sb.Append('\"');
}
}
NameValueHeaderValue.ToString(_extensions, ',', false, sb);
return sb.ToString();
}
public override bool Equals(object obj)
{
var other = obj as CacheControlHeaderValue;
if (other == null)
{
return false;
}
if ((_noCache != other._noCache) || (_noStore != other._noStore) || (_maxAge != other._maxAge) ||
(_sharedMaxAge != other._sharedMaxAge) || (_maxStale != other._maxStale) ||
(_maxStaleLimit != other._maxStaleLimit) || (_minFresh != other._minFresh) ||
(_noTransform != other._noTransform) || (_onlyIfCached != other._onlyIfCached) ||
(_public != other._public) || (_private != other._private) ||
(_mustRevalidate != other._mustRevalidate) || (_proxyRevalidate != other._proxyRevalidate))
{
return false;
}
if (!HeaderUtilities.AreEqualCollections(_noCacheHeaders, other._noCacheHeaders,
StringSegmentComparer.OrdinalIgnoreCase))
{
return false;
}
if (!HeaderUtilities.AreEqualCollections(_privateHeaders, other._privateHeaders,
StringSegmentComparer.OrdinalIgnoreCase))
{
return false;
}
if (!HeaderUtilities.AreEqualCollections(_extensions, other._extensions))
{
return false;
}
return true;
}
public override int GetHashCode()
{
// Use a different bit for bool fields: bool.GetHashCode() will return 0 (false) or 1 (true). So we would
// end up having the same hash code for e.g. two instances where one has only noCache set and the other
// only noStore.
int result = _noCache.GetHashCode() ^ (_noStore.GetHashCode() << 1) ^ (_maxStale.GetHashCode() << 2) ^
(_noTransform.GetHashCode() << 3) ^ (_onlyIfCached.GetHashCode() << 4) ^
(_public.GetHashCode() << 5) ^ (_private.GetHashCode() << 6) ^
(_mustRevalidate.GetHashCode() << 7) ^ (_proxyRevalidate.GetHashCode() << 8);
// XOR the hashcode of timespan values with different numbers to make sure two instances with the same
// timespan set on different fields result in different hashcodes.
result = result ^ (_maxAge.HasValue ? _maxAge.Value.GetHashCode() ^ 1 : 0) ^
(_sharedMaxAge.HasValue ? _sharedMaxAge.Value.GetHashCode() ^ 2 : 0) ^
(_maxStaleLimit.HasValue ? _maxStaleLimit.Value.GetHashCode() ^ 4 : 0) ^
(_minFresh.HasValue ? _minFresh.Value.GetHashCode() ^ 8 : 0);
if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0))
{
foreach (var noCacheHeader in _noCacheHeaders)
{
result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(noCacheHeader);
}
}
if ((_privateHeaders != null) && (_privateHeaders.Count > 0))
{
foreach (var privateHeader in _privateHeaders)
{
result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(privateHeader);
}
}
if ((_extensions != null) && (_extensions.Count > 0))
{
foreach (var extension in _extensions)
{
result = result ^ extension.GetHashCode();
}
}
return result;
}
public static CacheControlHeaderValue Parse(StringSegment input)
{
int index = 0;
// Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null.
var result = Parser.ParseValue(input, ref index);
if (result == null)
{
throw new FormatException("No cache directives found.");
}
return result;
}
public static bool TryParse(StringSegment input, out CacheControlHeaderValue parsedValue)
{
int index = 0;
// Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null.
if (Parser.TryParseValue(input, ref index, out parsedValue) && parsedValue != null)
{
return true;
}
parsedValue = null;
return false;
}
private static int GetCacheControlLength(StringSegment input, int startIndex, out CacheControlHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Cache-Control header consists of a list of name/value pairs, where the value is optional. So use an
// instance of NameValueHeaderParser to parse the string.
var current = startIndex;
NameValueHeaderValue nameValue = null;
var nameValueList = new List<NameValueHeaderValue>();
while (current < input.Length)
{
if (!NameValueHeaderValue.MultipleValueParser.TryParseValue(input, ref current, out nameValue))
{
return 0;
}
nameValueList.Add(nameValue);
}
// If we get here, we were able to successfully parse the string as list of name/value pairs. Now analyze
// the name/value pairs.
// Cache-Control is a header supporting lists of values. However, expose the header as an instance of
// CacheControlHeaderValue.
var result = new CacheControlHeaderValue();
if (!TrySetCacheControlValues(result, nameValueList))
{
return 0;
}
parsedValue = result;
// If we get here we successfully parsed the whole string.
return input.Length - startIndex;
}
private static bool TrySetCacheControlValues(
CacheControlHeaderValue cc,
List<NameValueHeaderValue> nameValueList)
{
for (var i = 0; i < nameValueList.Count; i++)
{
var nameValue = nameValueList[i];
var name = nameValue.Name;
var success = true;
switch (name.Length)
{
case 6:
if (StringSegment.Equals(PublicString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetTokenOnlyValue(nameValue, ref cc._public);
}
else
{
goto default;
}
break;
case 7:
if (StringSegment.Equals(MaxAgeString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetTimeSpan(nameValue, ref cc._maxAge);
}
else if(StringSegment.Equals(PrivateString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetOptionalTokenList(nameValue, ref cc._private, ref cc._privateHeaders);
}
else
{
goto default;
}
break;
case 8:
if (StringSegment.Equals(NoCacheString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetOptionalTokenList(nameValue, ref cc._noCache, ref cc._noCacheHeaders);
}
else if (StringSegment.Equals(NoStoreString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetTokenOnlyValue(nameValue, ref cc._noStore);
}
else if (StringSegment.Equals(SharedMaxAgeString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetTimeSpan(nameValue, ref cc._sharedMaxAge);
}
else
{
goto default;
}
break;
case 9:
if (StringSegment.Equals(MaxStaleString, name, StringComparison.OrdinalIgnoreCase))
{
success = ((nameValue.Value == null) || TrySetTimeSpan(nameValue, ref cc._maxStaleLimit));
if (success)
{
cc._maxStale = true;
}
}
else if (StringSegment.Equals(MinFreshString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetTimeSpan(nameValue, ref cc._minFresh);
}
else
{
goto default;
}
break;
case 12:
if (StringSegment.Equals(NoTransformString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetTokenOnlyValue(nameValue, ref cc._noTransform);
}
else
{
goto default;
}
break;
case 14:
if (StringSegment.Equals(OnlyIfCachedString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetTokenOnlyValue(nameValue, ref cc._onlyIfCached);
}
else
{
goto default;
}
break;
case 15:
if (StringSegment.Equals(MustRevalidateString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetTokenOnlyValue(nameValue, ref cc._mustRevalidate);
}
else
{
goto default;
}
break;
case 16:
if (StringSegment.Equals(ProxyRevalidateString, name, StringComparison.OrdinalIgnoreCase))
{
success = TrySetTokenOnlyValue(nameValue, ref cc._proxyRevalidate);
}
else
{
goto default;
}
break;
default:
cc.Extensions.Add(nameValue); // success is always true
break;
}
if (!success)
{
return false;
}
}
return true;
}
private static bool TrySetTokenOnlyValue(NameValueHeaderValue nameValue, ref bool boolField)
{
if (nameValue.Value != null)
{
return false;
}
boolField = true;
return true;
}
private static bool TrySetOptionalTokenList(
NameValueHeaderValue nameValue,
ref bool boolField,
ref ICollection<StringSegment> destination)
{
Contract.Requires(nameValue != null);
if (nameValue.Value == null)
{
boolField = true;
return true;
}
// We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we
// have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespaces.
var valueString = nameValue.Value;
if ((valueString.Length < 3) || (valueString[0] != '\"') || (valueString[valueString.Length - 1] != '\"'))
{
return false;
}
// We have a quoted string. Now verify that the string contains a list of valid tokens separated by ','.
var current = 1; // skip the initial '"' character.
var maxLength = valueString.Length - 1; // -1 because we don't want to parse the final '"'.
var separatorFound = false;
var originalValueCount = destination == null ? 0 : destination.Count;
while (current < maxLength)
{
current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(valueString, current, true,
out separatorFound);
if (current == maxLength)
{
break;
}
var tokenLength = HttpRuleParser.GetTokenLength(valueString, current);
if (tokenLength == 0)
{
// We already skipped whitespaces and separators. If we don't have a token it must be an invalid
// character.
return false;
}
if (destination == null)
{
destination = new ObjectCollection<StringSegment>(CheckIsValidTokenAction);
}
destination.Add(valueString.Subsegment(current, tokenLength));
current = current + tokenLength;
}
// After parsing a valid token list, we expect to have at least one value
if ((destination != null) && (destination.Count > originalValueCount))
{
boolField = true;
return true;
}
return false;
}
private static bool TrySetTimeSpan(NameValueHeaderValue nameValue, ref TimeSpan? timeSpan)
{
Contract.Requires(nameValue != null);
if (nameValue.Value == null)
{
return false;
}
int seconds;
if (!HeaderUtilities.TryParseNonNegativeInt32(nameValue.Value, out seconds))
{
return false;
}
timeSpan = new TimeSpan(0, 0, seconds);
return true;
}
private static void AppendValueIfRequired(StringBuilder sb, bool appendValue, string value)
{
if (appendValue)
{
AppendValueWithSeparatorIfRequired(sb, value);
}
}
private static void AppendValueWithSeparatorIfRequired(StringBuilder sb, string value)
{
if (sb.Length > 0)
{
sb.Append(", ");
}
sb.Append(value);
}
private static void AppendValues(StringBuilder sb, IEnumerable<StringSegment> values)
{
var first = true;
foreach (var value in values)
{
if (first)
{
first = false;
}
else
{
sb.Append(", ");
}
sb.Append(value);
}
}
private static void CheckIsValidToken(StringSegment item)
{
HeaderUtilities.CheckValidToken(item, nameof(item));
}
}
}

View File

@ -0,0 +1,725 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
// Note this is for use both in HTTP (https://tools.ietf.org/html/rfc6266) and MIME (https://tools.ietf.org/html/rfc2183)
public class ContentDispositionHeaderValue
{
private const string FileNameString = "filename";
private const string NameString = "name";
private const string FileNameStarString = "filename*";
private const string CreationDateString = "creation-date";
private const string ModificationDateString = "modification-date";
private const string ReadDateString = "read-date";
private const string SizeString = "size";
private static readonly char[] QuestionMark = new char[] { '?' };
private static readonly char[] SingleQuote = new char[] { '\'' };
private static readonly HttpHeaderParser<ContentDispositionHeaderValue> Parser
= new GenericHeaderParser<ContentDispositionHeaderValue>(false, GetDispositionTypeLength);
// Use list instead of dictionary since we may have multiple parameters with the same name.
private ObjectCollection<NameValueHeaderValue> _parameters;
private StringSegment _dispositionType;
private ContentDispositionHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
public ContentDispositionHeaderValue(StringSegment dispositionType)
{
CheckDispositionTypeFormat(dispositionType, "dispositionType");
_dispositionType = dispositionType;
}
public StringSegment DispositionType
{
get { return _dispositionType; }
set
{
CheckDispositionTypeFormat(value, "value");
_dispositionType = value;
}
}
public IList<NameValueHeaderValue> Parameters
{
get
{
if (_parameters == null)
{
_parameters = new ObjectCollection<NameValueHeaderValue>();
}
return _parameters;
}
}
// Helpers to access specific parameters in the list
public StringSegment Name
{
get { return GetName(NameString); }
set { SetName(NameString, value); }
}
public StringSegment FileName
{
get { return GetName(FileNameString); }
set { SetName(FileNameString, value); }
}
public StringSegment FileNameStar
{
get { return GetName(FileNameStarString); }
set { SetName(FileNameStarString, value); }
}
public DateTimeOffset? CreationDate
{
get { return GetDate(CreationDateString); }
set { SetDate(CreationDateString, value); }
}
public DateTimeOffset? ModificationDate
{
get { return GetDate(ModificationDateString); }
set { SetDate(ModificationDateString, value); }
}
public DateTimeOffset? ReadDate
{
get { return GetDate(ReadDateString); }
set { SetDate(ReadDateString, value); }
}
public long? Size
{
get
{
var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString);
long value;
if (sizeParameter != null)
{
var sizeString = sizeParameter.Value;
if (HeaderUtilities.TryParseNonNegativeInt64(sizeString, out value))
{
return value;
}
}
return null;
}
set
{
var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString);
if (value == null)
{
// Remove parameter
if (sizeParameter != null)
{
_parameters.Remove(sizeParameter);
}
}
else if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
else if (sizeParameter != null)
{
sizeParameter.Value = value.Value.ToString(CultureInfo.InvariantCulture);
}
else
{
string sizeString = value.Value.ToString(CultureInfo.InvariantCulture);
_parameters.Add(new NameValueHeaderValue(SizeString, sizeString));
}
}
}
/// <summary>
/// Sets both FileName and FileNameStar using encodings appropriate for HTTP headers.
/// </summary>
/// <param name="fileName"></param>
public void SetHttpFileName(StringSegment fileName)
{
if (!StringSegment.IsNullOrEmpty(fileName))
{
FileName = Sanatize(fileName);
}
else
{
FileName = fileName;
}
FileNameStar = fileName;
}
/// <summary>
/// Sets the FileName parameter using encodings appropriate for MIME headers.
/// The FileNameStar paraemter is removed.
/// </summary>
/// <param name="fileName"></param>
public void SetMimeFileName(StringSegment fileName)
{
FileNameStar = null;
FileName = fileName;
}
public override string ToString()
{
return _dispositionType + NameValueHeaderValue.ToString(_parameters, ';', true);
}
public override bool Equals(object obj)
{
var other = obj as ContentDispositionHeaderValue;
if (other == null)
{
return false;
}
return _dispositionType.Equals(other._dispositionType, StringComparison.OrdinalIgnoreCase) &&
HeaderUtilities.AreEqualCollections(_parameters, other._parameters);
}
public override int GetHashCode()
{
// The dispositionType string is case-insensitive.
return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_dispositionType) ^ NameValueHeaderValue.GetHashCode(_parameters);
}
public static ContentDispositionHeaderValue Parse(StringSegment input)
{
var index = 0;
return Parser.ParseValue(input, ref index);
}
public static bool TryParse(StringSegment input, out ContentDispositionHeaderValue parsedValue)
{
var index = 0;
return Parser.TryParseValue(input, ref index, out parsedValue);
}
private static int GetDispositionTypeLength(StringSegment input, int startIndex, out ContentDispositionHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Caller must remove leading whitespaces. If not, we'll return 0.
var dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out var dispositionType);
if (dispositionTypeLength == 0)
{
return 0;
}
var current = startIndex + dispositionTypeLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
var contentDispositionHeader = new ContentDispositionHeaderValue();
contentDispositionHeader._dispositionType = dispositionType;
// If we're not done and we have a parameter delimiter, then we have a list of parameters.
if ((current < input.Length) && (input[current] == ';'))
{
current++; // skip delimiter.
int parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';',
contentDispositionHeader.Parameters);
parsedValue = contentDispositionHeader;
return current + parameterLength - startIndex;
}
// We have a ContentDisposition header without parameters.
parsedValue = contentDispositionHeader;
return current - startIndex;
}
private static int GetDispositionTypeExpressionLength(StringSegment input, int startIndex, out StringSegment dispositionType)
{
Contract.Requires((input != null) && (input.Length > 0) && (startIndex < input.Length));
// This method just parses the disposition type string, it does not parse parameters.
dispositionType = null;
// Parse the disposition type, i.e. <dispositiontype> in content-disposition string
// "<dispositiontype>; param1=value1; param2=value2"
var typeLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (typeLength == 0)
{
return 0;
}
dispositionType = input.Subsegment(startIndex, typeLength);
return typeLength;
}
private static void CheckDispositionTypeFormat(StringSegment dispositionType, string parameterName)
{
if (StringSegment.IsNullOrEmpty(dispositionType))
{
throw new ArgumentException("An empty string is not allowed.", parameterName);
}
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
var dispositionTypeLength = GetDispositionTypeExpressionLength(dispositionType, 0, out var tempDispositionType);
if ((dispositionTypeLength == 0) || (tempDispositionType.Length != dispositionType.Length))
{
throw new FormatException(string.Format(CultureInfo.InvariantCulture,
"Invalid disposition type '{0}'.", dispositionType));
}
}
// Gets a parameter of the given name and attempts to extract a date.
// Returns null if the parameter is not present or the format is incorrect.
private DateTimeOffset? GetDate(string parameter)
{
var dateParameter = NameValueHeaderValue.Find(_parameters, parameter);
if (dateParameter != null)
{
var dateString = dateParameter.Value;
// Should have quotes, remove them.
if (IsQuoted(dateString))
{
dateString = dateString.Subsegment(1, dateString.Length - 2);
}
DateTimeOffset date;
if (HttpRuleParser.TryStringToDate(dateString, out date))
{
return date;
}
}
return null;
}
// Add the given parameter to the list. Remove if date is null.
private void SetDate(string parameter, DateTimeOffset? date)
{
var dateParameter = NameValueHeaderValue.Find(_parameters, parameter);
if (date == null)
{
// Remove parameter
if (dateParameter != null)
{
_parameters.Remove(dateParameter);
}
}
else
{
// Must always be quoted
var dateString = HeaderUtilities.FormatDate(date.Value, quoted: true);
if (dateParameter != null)
{
dateParameter.Value = dateString;
}
else
{
Parameters.Add(new NameValueHeaderValue(parameter, dateString));
}
}
}
// Gets a parameter of the given name and attempts to decode it if necessary.
// Returns null if the parameter is not present or the raw value if the encoding is incorrect.
private StringSegment GetName(string parameter)
{
var nameParameter = NameValueHeaderValue.Find(_parameters, parameter);
if (nameParameter != null)
{
string result;
// filename*=utf-8'lang'%7FMyString
if (parameter.EndsWith("*", StringComparison.Ordinal))
{
if (TryDecode5987(nameParameter.Value, out result))
{
return result;
}
return null; // Unrecognized encoding
}
// filename="=?utf-8?B?BDFSDFasdfasdc==?="
if (TryDecodeMime(nameParameter.Value, out result))
{
return result;
}
// May not have been encoded
return HeaderUtilities.RemoveQuotes(nameParameter.Value);
}
return null;
}
// Add/update the given parameter in the list, encoding if necessary.
// Remove if value is null/Empty
private void SetName(StringSegment parameter, StringSegment value)
{
var nameParameter = NameValueHeaderValue.Find(_parameters, parameter);
if (StringSegment.IsNullOrEmpty(value))
{
// Remove parameter
if (nameParameter != null)
{
_parameters.Remove(nameParameter);
}
}
else
{
var processedValue = StringSegment.Empty;
if (parameter.EndsWith("*", StringComparison.Ordinal))
{
processedValue = Encode5987(value);
}
else
{
processedValue = EncodeAndQuoteMime(value);
}
if (nameParameter != null)
{
nameParameter.Value = processedValue;
}
else
{
Parameters.Add(new NameValueHeaderValue(parameter, processedValue));
}
}
}
// Returns input for decoding failures, as the content might not be encoded
private StringSegment EncodeAndQuoteMime(StringSegment input)
{
var result = input;
var needsQuotes = false;
// Remove bounding quotes, they'll get re-added later
if (IsQuoted(result))
{
result = result.Subsegment(1, result.Length - 2);
needsQuotes = true;
}
if (RequiresEncoding(result))
{
needsQuotes = true; // Encoded data must always be quoted, the equals signs are invalid in tokens
result = EncodeMime(result); // =?utf-8?B?asdfasdfaesdf?=
}
else if (!needsQuotes && HttpRuleParser.GetTokenLength(result, 0) != result.Length)
{
needsQuotes = true;
}
if (needsQuotes)
{
// '\' and '"' must be escaped in a quoted string
result = result.ToString().Replace(@"\", @"\\").Replace(@"""", @"\""");
// Re-add quotes "value"
result = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", result);
}
return result;
}
// Replaces characters not suitable for HTTP headers with '_' rather than MIME encoding them.
private StringSegment Sanatize(StringSegment input)
{
var result = input;
if (RequiresEncoding(result))
{
var builder = new StringBuilder(result.Length);
for (int i = 0; i < result.Length; i++)
{
var c = result[i];
if ((int)c > 0x7f)
{
c = '_'; // Replace out-of-range characters
}
builder.Append(c);
}
result = builder.ToString();
}
return result;
}
// Returns true if the value starts and ends with a quote
private bool IsQuoted(StringSegment value)
{
Contract.Assert(value != null);
return value.Length > 1 && value.StartsWith("\"", StringComparison.Ordinal)
&& value.EndsWith("\"", StringComparison.Ordinal);
}
// tspecials are required to be in a quoted string. Only non-ascii needs to be encoded.
private bool RequiresEncoding(StringSegment input)
{
Contract.Assert(input != null);
for (int i = 0; i < input.Length; i++)
{
if ((int)input[i] > 0x7f)
{
return true;
}
}
return false;
}
// Encode using MIME encoding
private unsafe string EncodeMime(StringSegment input)
{
fixed (char* chars = input.Buffer)
{
var byteCount = Encoding.UTF8.GetByteCount(chars + input.Offset, input.Length);
var buffer = new byte[byteCount];
fixed (byte* bytes = buffer)
{
Encoding.UTF8.GetBytes(chars + input.Offset, input.Length, bytes, byteCount);
}
var encodedName = Convert.ToBase64String(buffer);
return "=?utf-8?B?" + encodedName + "?=";
}
}
// Attempt to decode MIME encoded strings
private bool TryDecodeMime(StringSegment input, out string output)
{
Contract.Assert(input != null);
output = null;
var processedInput = input;
// Require quotes, min of "=?e?b??="
if (!IsQuoted(processedInput) || processedInput.Length < 10)
{
return false;
}
var parts = processedInput.Split(QuestionMark).ToArray();
// "=, encodingName, encodingType, encodedData, ="
if (parts.Length != 5 || parts[0] != "\"=" || parts[4] != "=\""
|| !parts[2].Equals("b", StringComparison.OrdinalIgnoreCase))
{
// Not encoded.
// This does not support multi-line encoding.
// Only base64 encoding is supported, not quoted printable
return false;
}
try
{
var encoding = Encoding.GetEncoding(parts[1].ToString());
var bytes = Convert.FromBase64String(parts[3].ToString());
output = encoding.GetString(bytes, 0, bytes.Length);
return true;
}
catch (ArgumentException)
{
// Unknown encoding or bad characters
}
catch (FormatException)
{
// Bad base64 decoding
}
return false;
}
// Encode a string using RFC 5987 encoding
// encoding'lang'PercentEncodedSpecials
private string Encode5987(StringSegment input)
{
var builder = new StringBuilder("UTF-8\'\'");
for (int i = 0; i < input.Length; i++)
{
var c = input[i];
// attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
// ; token except ( "*" / "'" / "%" )
if (c > 0x7F) // Encodes as multiple utf-8 bytes
{
var bytes = Encoding.UTF8.GetBytes(c.ToString());
foreach (byte b in bytes)
{
HexEscape(builder, (char)b);
}
}
else if (!HttpRuleParser.IsTokenChar(c) || c == '*' || c == '\'' || c == '%')
{
// ASCII - Only one encoded byte
HexEscape(builder, c);
}
else
{
builder.Append(c);
}
}
return builder.ToString();
}
private static readonly char[] HexUpperChars = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
private static void HexEscape(StringBuilder builder, char c)
{
builder.Append('%');
builder.Append(HexUpperChars[(c & 0xf0) >> 4]);
builder.Append(HexUpperChars[c & 0xf]);
}
// Attempt to decode using RFC 5987 encoding.
// encoding'language'my%20string
private bool TryDecode5987(StringSegment input, out string output)
{
output = null;
var parts = input.Split(SingleQuote).ToArray();
if (parts.Length != 3)
{
return false;
}
var decoded = new StringBuilder();
byte[] unescapedBytes = null;
try
{
var encoding = Encoding.GetEncoding(parts[0].ToString());
var dataString = parts[2];
unescapedBytes = ArrayPool<byte>.Shared.Rent(dataString.Length);
var unescapedBytesCount = 0;
for (var index = 0; index < dataString.Length; index++)
{
if (IsHexEncoding(dataString, index)) // %FF
{
// Unescape and cache bytes, multi-byte characters must be decoded all at once
unescapedBytes[unescapedBytesCount++] = HexUnescape(dataString, ref index);
index--; // HexUnescape did +=3; Offset the for loop's ++
}
else
{
if (unescapedBytesCount > 0)
{
// Decode any previously cached bytes
decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount));
unescapedBytesCount = 0;
}
decoded.Append(dataString[index]); // Normal safe character
}
}
if (unescapedBytesCount > 0)
{
// Decode any previously cached bytes
decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount));
}
}
catch (ArgumentException)
{
return false; // Unknown encoding or bad characters
}
finally
{
if (unescapedBytes != null)
{
ArrayPool<byte>.Shared.Return(unescapedBytes);
}
}
output = decoded.ToString();
return true;
}
private static bool IsHexEncoding(StringSegment pattern, int index)
{
if ((pattern.Length - index) < 3)
{
return false;
}
if ((pattern[index] == '%') && IsEscapedAscii(pattern[index + 1], pattern[index + 2]))
{
return true;
}
return false;
}
private static bool IsEscapedAscii(char digit, char next)
{
if (!(((digit >= '0') && (digit <= '9'))
|| ((digit >= 'A') && (digit <= 'F'))
|| ((digit >= 'a') && (digit <= 'f'))))
{
return false;
}
if (!(((next >= '0') && (next <= '9'))
|| ((next >= 'A') && (next <= 'F'))
|| ((next >= 'a') && (next <= 'f'))))
{
return false;
}
return true;
}
private static byte HexUnescape(StringSegment pattern, ref int index)
{
if ((index < 0) || (index >= pattern.Length))
{
throw new ArgumentOutOfRangeException(nameof(index));
}
if ((pattern[index] == '%')
&& (pattern.Length - index >= 3))
{
var ret = UnEscapeAscii(pattern[index + 1], pattern[index + 2]);
index += 3;
return ret;
}
return (byte)pattern[index++];
}
internal static byte UnEscapeAscii(char digit, char next)
{
if (!(((digit >= '0') && (digit <= '9'))
|| ((digit >= 'A') && (digit <= 'F'))
|| ((digit >= 'a') && (digit <= 'f'))))
{
throw new ArgumentException();
}
var res = (digit <= '9')
? ((int)digit - (int)'0')
: (((digit <= 'F')
? ((int)digit - (int)'A')
: ((int)digit - (int)'a'))
+ 10);
if (!(((next >= '0') && (next <= '9'))
|| ((next >= 'A') && (next <= 'F'))
|| ((next >= 'a') && (next <= 'f'))))
{
throw new ArgumentException();
}
return (byte)((res << 4) + ((next <= '9')
? ((int)next - (int)'0')
: (((next <= 'F')
? ((int)next - (int)'A')
: ((int)next - (int)'a'))
+ 10)));
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
/// <summary>
/// Various extension methods for <see cref="ContentDispositionHeaderValue"/> for identifying the type of the disposition header
/// </summary>
public static class ContentDispositionHeaderValueIdentityExtensions
{
/// <summary>
/// Checks if the content disposition header is a file disposition
/// </summary>
/// <param name="header">The header to check</param>
/// <returns>True if the header is file disposition, false otherwise</returns>
public static bool IsFileDisposition(this ContentDispositionHeaderValue header)
{
if (header == null)
{
throw new ArgumentNullException(nameof(header));
}
return header.DispositionType.Equals("form-data")
&& (!StringSegment.IsNullOrEmpty(header.FileName) || !StringSegment.IsNullOrEmpty(header.FileNameStar));
}
/// <summary>
/// Checks if the content disposition header is a form disposition
/// </summary>
/// <param name="header">The header to check</param>
/// <returns>True if the header is form disposition, false otherwise</returns>
public static bool IsFormDisposition(this ContentDispositionHeaderValue header)
{
if (header == null)
{
throw new ArgumentNullException(nameof(header));
}
return header.DispositionType.Equals("form-data")
&& StringSegment.IsNullOrEmpty(header.FileName) && StringSegment.IsNullOrEmpty(header.FileNameStar);
}
}
}

View File

@ -0,0 +1,407 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
public class ContentRangeHeaderValue
{
private static readonly HttpHeaderParser<ContentRangeHeaderValue> Parser
= new GenericHeaderParser<ContentRangeHeaderValue>(false, GetContentRangeLength);
private StringSegment _unit;
private long? _from;
private long? _to;
private long? _length;
private ContentRangeHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
public ContentRangeHeaderValue(long from, long to, long length)
{
// Scenario: "Content-Range: bytes 12-34/5678"
if (length < 0)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if ((to < 0) || (to > length))
{
throw new ArgumentOutOfRangeException(nameof(to));
}
if ((from < 0) || (from > to))
{
throw new ArgumentOutOfRangeException(nameof(from));
}
_from = from;
_to = to;
_length = length;
_unit = HeaderUtilities.BytesUnit;
}
public ContentRangeHeaderValue(long length)
{
// Scenario: "Content-Range: bytes */1234"
if (length < 0)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
_length = length;
_unit = HeaderUtilities.BytesUnit;
}
public ContentRangeHeaderValue(long from, long to)
{
// Scenario: "Content-Range: bytes 12-34/*"
if (to < 0)
{
throw new ArgumentOutOfRangeException(nameof(to));
}
if ((from < 0) || (from > to))
{
throw new ArgumentOutOfRangeException(nameof(@from));
}
_from = from;
_to = to;
_unit = HeaderUtilities.BytesUnit;
}
public StringSegment Unit
{
get { return _unit; }
set
{
HeaderUtilities.CheckValidToken(value, nameof(value));
_unit = value;
}
}
public long? From
{
get { return _from; }
}
public long? To
{
get { return _to; }
}
public long? Length
{
get { return _length; }
}
public bool HasLength // e.g. "Content-Range: bytes 12-34/*"
{
get { return _length != null; }
}
public bool HasRange // e.g. "Content-Range: bytes */1234"
{
get { return _from != null; }
}
public override bool Equals(object obj)
{
var other = obj as ContentRangeHeaderValue;
if (other == null)
{
return false;
}
return ((_from == other._from) && (_to == other._to) && (_length == other._length) &&
StringSegment.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase));
}
public override int GetHashCode()
{
var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_unit);
if (HasRange)
{
result = result ^ _from.GetHashCode() ^ _to.GetHashCode();
}
if (HasLength)
{
result = result ^ _length.GetHashCode();
}
return result;
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append(_unit);
sb.Append(' ');
if (HasRange)
{
sb.Append(_from.Value.ToString(NumberFormatInfo.InvariantInfo));
sb.Append('-');
sb.Append(_to.Value.ToString(NumberFormatInfo.InvariantInfo));
}
else
{
sb.Append('*');
}
sb.Append('/');
if (HasLength)
{
sb.Append(_length.Value.ToString(NumberFormatInfo.InvariantInfo));
}
else
{
sb.Append('*');
}
return sb.ToString();
}
public static ContentRangeHeaderValue Parse(StringSegment input)
{
var index = 0;
return Parser.ParseValue(input, ref index);
}
public static bool TryParse(StringSegment input, out ContentRangeHeaderValue parsedValue)
{
var index = 0;
return Parser.TryParseValue(input, ref index, out parsedValue);
}
private static int GetContentRangeLength(StringSegment input, int startIndex, out ContentRangeHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Parse the unit string: <unit> in '<unit> <from>-<to>/<length>'
var unitLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (unitLength == 0)
{
return 0;
}
var unit = input.Subsegment(startIndex, unitLength);
var current = startIndex + unitLength;
var separatorLength = HttpRuleParser.GetWhitespaceLength(input, current);
if (separatorLength == 0)
{
return 0;
}
current = current + separatorLength;
if (current == input.Length)
{
return 0;
}
// Read range values <from> and <to> in '<unit> <from>-<to>/<length>'
var fromStartIndex = current;
var fromLength = 0;
var toStartIndex = 0;
var toLength = 0;
if (!TryGetRangeLength(input, ref current, out fromLength, out toStartIndex, out toLength))
{
return 0;
}
// After the range is read we expect the length separator '/'
if ((current == input.Length) || (input[current] != '/'))
{
return 0;
}
current++; // Skip '/' separator
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
if (current == input.Length)
{
return 0;
}
// We may not have a length (e.g. 'bytes 1-2/*'). But if we do, parse the length now.
var lengthStartIndex = current;
var lengthLength = 0;
if (!TryGetLengthLength(input, ref current, out lengthLength))
{
return 0;
}
if (!TryCreateContentRange(input, unit, fromStartIndex, fromLength, toStartIndex, toLength,
lengthStartIndex, lengthLength, out parsedValue))
{
return 0;
}
return current - startIndex;
}
private static bool TryGetLengthLength(StringSegment input, ref int current, out int lengthLength)
{
lengthLength = 0;
if (input[current] == '*')
{
current++;
}
else
{
// Parse length value: <length> in '<unit> <from>-<to>/<length>'
lengthLength = HttpRuleParser.GetNumberLength(input, current, false);
if ((lengthLength == 0) || (lengthLength > HttpRuleParser.MaxInt64Digits))
{
return false;
}
current = current + lengthLength;
}
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
return true;
}
private static bool TryGetRangeLength(StringSegment input, ref int current, out int fromLength, out int toStartIndex, out int toLength)
{
fromLength = 0;
toStartIndex = 0;
toLength = 0;
// Check if we have a value like 'bytes */133'. If yes, skip the range part and continue parsing the
// length separator '/'.
if (input[current] == '*')
{
current++;
}
else
{
// Parse first range value: <from> in '<unit> <from>-<to>/<length>'
fromLength = HttpRuleParser.GetNumberLength(input, current, false);
if ((fromLength == 0) || (fromLength > HttpRuleParser.MaxInt64Digits))
{
return false;
}
current = current + fromLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
// After the first value, the '-' character must follow.
if ((current == input.Length) || (input[current] != '-'))
{
// We need a '-' character otherwise this can't be a valid range.
return false;
}
current++; // skip the '-' character
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
if (current == input.Length)
{
return false;
}
// Parse second range value: <to> in '<unit> <from>-<to>/<length>'
toStartIndex = current;
toLength = HttpRuleParser.GetNumberLength(input, current, false);
if ((toLength == 0) || (toLength > HttpRuleParser.MaxInt64Digits))
{
return false;
}
current = current + toLength;
}
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
return true;
}
private static bool TryCreateContentRange(
StringSegment input,
StringSegment unit,
int fromStartIndex,
int fromLength,
int toStartIndex,
int toLength,
int lengthStartIndex,
int lengthLength,
out ContentRangeHeaderValue parsedValue)
{
parsedValue = null;
long from = 0;
if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from))
{
return false;
}
long to = 0;
if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to))
{
return false;
}
// 'from' must not be greater than 'to'
if ((fromLength > 0) && (toLength > 0) && (from > to))
{
return false;
}
long length = 0;
if ((lengthLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(lengthStartIndex, lengthLength),
out length))
{
return false;
}
// 'from' and 'to' must be less than 'length'
if ((toLength > 0) && (lengthLength > 0) && (to >= length))
{
return false;
}
var result = new ContentRangeHeaderValue();
result._unit = unit;
if (fromLength > 0)
{
result._from = from;
result._to = to;
}
if (lengthLength > 0)
{
result._length = length;
}
parsedValue = result;
return true;
}
}
}

View File

@ -0,0 +1,98 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics.Contracts;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
internal class CookieHeaderParser : HttpHeaderParser<CookieHeaderValue>
{
internal CookieHeaderParser(bool supportsMultipleValues)
: base(supportsMultipleValues)
{
}
public sealed override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue parsedValue)
{
parsedValue = null;
// If multiple values are supported (i.e. list of values), then accept an empty string: The header may
// be added multiple times to the request/response message. E.g.
// Accept: text/xml; q=1
// Accept:
// Accept: text/plain; q=0.2
if (StringSegment.IsNullOrEmpty(value) || (index == value.Length))
{
return SupportsMultipleValues;
}
var separatorFound = false;
var current = GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, out separatorFound);
if (separatorFound && !SupportsMultipleValues)
{
return false; // leading separators not allowed if we don't support multiple values.
}
if (current == value.Length)
{
if (SupportsMultipleValues)
{
index = current;
}
return SupportsMultipleValues;
}
CookieHeaderValue result = null;
if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out result))
{
return false;
}
current = GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, out separatorFound);
// If we support multiple values and we've not reached the end of the string, then we must have a separator.
if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length)))
{
return false;
}
index = current;
parsedValue = result;
return true;
}
private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound)
{
Contract.Requires(input != null);
Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length.
separatorFound = false;
var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
if ((current == input.Length) || (input[current] != ',' && input[current] != ';'))
{
return current;
}
// If we have a separator, skip the separator and all following whitespaces. If we support
// empty values, continue until the current character is neither a separator nor a whitespace.
separatorFound = true;
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
if (skipEmptyValues)
{
// Most headers only split on ',', but cookies primarily split on ';'
while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';')))
{
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
}
}
return current;
}
}
}

View File

@ -0,0 +1,277 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
// http://tools.ietf.org/html/rfc6265
public class CookieHeaderValue
{
private static readonly CookieHeaderParser SingleValueParser = new CookieHeaderParser(supportsMultipleValues: false);
private static readonly CookieHeaderParser MultipleValueParser = new CookieHeaderParser(supportsMultipleValues: true);
private StringSegment _name;
private StringSegment _value;
private CookieHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
public CookieHeaderValue(StringSegment name)
: this(name, StringSegment.Empty)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
}
public CookieHeaderValue(StringSegment name, StringSegment value)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
Name = name;
Value = value;
}
public StringSegment Name
{
get { return _name; }
set
{
CheckNameFormat(value, nameof(value));
_name = value;
}
}
public StringSegment Value
{
get { return _value; }
set
{
CheckValueFormat(value, nameof(value));
_value = value;
}
}
// name="val ue";
public override string ToString()
{
var header = new StringBuilder();
header.Append(_name);
header.Append("=");
header.Append(_value);
return header.ToString();
}
public static CookieHeaderValue Parse(StringSegment input)
{
var index = 0;
return SingleValueParser.ParseValue(input, ref index);
}
public static bool TryParse(StringSegment input, out CookieHeaderValue parsedValue)
{
var index = 0;
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
}
public static IList<CookieHeaderValue> ParseList(IList<string> inputs)
{
return MultipleValueParser.ParseValues(inputs);
}
public static IList<CookieHeaderValue> ParseStrictList(IList<string> inputs)
{
return MultipleValueParser.ParseStrictValues(inputs);
}
public static bool TryParseList(IList<string> inputs, out IList<CookieHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
}
public static bool TryParseStrictList(IList<string> inputs, out IList<CookieHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
}
// name=value; name="value"
internal static bool TryGetCookieLength(StringSegment input, ref int offset, out CookieHeaderValue parsedValue)
{
Contract.Requires(offset >= 0);
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length))
{
return false;
}
var result = new CookieHeaderValue();
// The caller should have already consumed any leading whitespace, commas, etc..
// Name=value;
// Name
var itemLength = HttpRuleParser.GetTokenLength(input, offset);
if (itemLength == 0)
{
return false;
}
result._name = input.Subsegment(offset, itemLength);
offset += itemLength;
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return false;
}
// value or "quoted value"
// The value may be empty
result._value = GetCookieValue(input, ref offset);
parsedValue = result;
return true;
}
// cookie-value = *cookie-octet / ( DQUOTE* cookie-octet DQUOTE )
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
// ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash
internal static StringSegment GetCookieValue(StringSegment input, ref int offset)
{
Contract.Requires(input != null);
Contract.Requires(offset >= 0);
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - offset)));
var startIndex = offset;
if (offset >= input.Length)
{
return StringSegment.Empty;
}
var inQuotes = false;
if (input[offset] == '"')
{
inQuotes = true;
offset++;
}
while (offset < input.Length)
{
var c = input[offset];
if (!IsCookieValueChar(c))
{
break;
}
offset++;
}
if (inQuotes)
{
if (offset == input.Length || input[offset] != '"')
{
// Missing final quote
return StringSegment.Empty;
}
offset++;
}
int length = offset - startIndex;
if (offset > startIndex)
{
return input.Subsegment(startIndex, length);
}
return StringSegment.Empty;
}
private static bool ReadEqualsSign(StringSegment input, ref int offset)
{
// = (no spaces)
if (offset >= input.Length || input[offset] != '=')
{
return false;
}
offset++;
return true;
}
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
// ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash
private static bool IsCookieValueChar(char c)
{
if (c < 0x21 || c > 0x7E)
{
return false;
}
return !(c == '"' || c == ',' || c == ';' || c == '\\');
}
internal static void CheckNameFormat(StringSegment name, string parameterName)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (HttpRuleParser.GetTokenLength(name, 0) != name.Length)
{
throw new ArgumentException("Invalid cookie name: " + name, parameterName);
}
}
internal static void CheckValueFormat(StringSegment value, string parameterName)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
var offset = 0;
var result = GetCookieValue(value, ref offset);
if (result.Length != value.Length)
{
throw new ArgumentException("Invalid cookie value: " + value, parameterName);
}
}
public override bool Equals(object obj)
{
var other = obj as CookieHeaderValue;
if (other == null)
{
return false;
}
return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase)
&& StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase);
}
public override int GetHashCode()
{
return _name.GetHashCode() ^ _value.GetHashCode();
}
}
}

View File

@ -0,0 +1,100 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
internal static class DateTimeFormatter
{
private static readonly DateTimeFormatInfo FormatInfo = CultureInfo.InvariantCulture.DateTimeFormat;
private static readonly string[] MonthNames = FormatInfo.AbbreviatedMonthNames;
private static readonly string[] DayNames = FormatInfo.AbbreviatedDayNames;
private static readonly int Rfc1123DateLength = "ddd, dd MMM yyyy HH:mm:ss GMT".Length;
private static readonly int QuotedRfc1123DateLength = Rfc1123DateLength + 2;
// ASCII numbers are in the range 48 - 57.
private const int AsciiNumberOffset = 0x30;
private const string Gmt = "GMT";
private const char Comma = ',';
private const char Space = ' ';
private const char Colon = ':';
private const char Quote = '"';
public static string ToRfc1123String(this DateTimeOffset dateTime)
{
return ToRfc1123String(dateTime, false);
}
public static string ToRfc1123String(this DateTimeOffset dateTime, bool quoted)
{
var universal = dateTime.UtcDateTime;
var length = quoted ? QuotedRfc1123DateLength : Rfc1123DateLength;
var target = new InplaceStringBuilder(length);
if (quoted)
{
target.Append(Quote);
}
target.Append(DayNames[(int)universal.DayOfWeek]);
target.Append(Comma);
target.Append(Space);
AppendNumber(ref target, universal.Day);
target.Append(Space);
target.Append(MonthNames[universal.Month - 1]);
target.Append(Space);
AppendYear(ref target, universal.Year);
target.Append(Space);
AppendTimeOfDay(ref target, universal.TimeOfDay);
target.Append(Space);
target.Append(Gmt);
if (quoted)
{
target.Append(Quote);
}
return target.ToString();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AppendYear(ref InplaceStringBuilder target, int year)
{
target.Append(GetAsciiChar(year / 1000));
target.Append(GetAsciiChar(year % 1000 / 100));
target.Append(GetAsciiChar(year % 100 / 10));
target.Append(GetAsciiChar(year % 10));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AppendTimeOfDay(ref InplaceStringBuilder target, TimeSpan timeOfDay)
{
AppendNumber(ref target, timeOfDay.Hours);
target.Append(Colon);
AppendNumber(ref target, timeOfDay.Minutes);
target.Append(Colon);
AppendNumber(ref target, timeOfDay.Seconds);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AppendNumber(ref InplaceStringBuilder target, int number)
{
target.Append(GetAsciiChar(number / 10));
target.Append(GetAsciiChar(number % 10));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static char GetAsciiChar(int value)
{
return (char)(AsciiNumberOffset + value);
}
}
}

View File

@ -0,0 +1,250 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
public class EntityTagHeaderValue
{
// Note that the ETag header does not allow a * but we're not that strict: We allow both '*' and ETag values in a single value.
// We can't guarantee that a single parsed value will be used directly in an ETag header.
private static readonly HttpHeaderParser<EntityTagHeaderValue> SingleValueParser
= new GenericHeaderParser<EntityTagHeaderValue>(false, GetEntityTagLength);
// Note that if multiple ETag values are allowed (e.g. 'If-Match', 'If-None-Match'), according to the RFC
// the value must either be '*' or a list of ETag values. It's not allowed to have both '*' and a list of
// ETag values. We're not that strict: We allow both '*' and ETag values in a list. If the server sends such
// an invalid list, we want to be able to represent it using the corresponding header property.
private static readonly HttpHeaderParser<EntityTagHeaderValue> MultipleValueParser
= new GenericHeaderParser<EntityTagHeaderValue>(true, GetEntityTagLength);
private static EntityTagHeaderValue AnyType;
private StringSegment _tag;
private bool _isWeak;
private EntityTagHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
public EntityTagHeaderValue(StringSegment tag)
: this(tag, false)
{
}
public EntityTagHeaderValue(StringSegment tag, bool isWeak)
{
if (StringSegment.IsNullOrEmpty(tag))
{
throw new ArgumentException("An empty string is not allowed.", nameof(tag));
}
int length = 0;
if (!isWeak && StringSegment.Equals(tag, "*", StringComparison.Ordinal))
{
// * is valid, but W/* isn't.
_tag = tag;
}
else if ((HttpRuleParser.GetQuotedStringLength(tag, 0, out length) != HttpParseResult.Parsed) ||
(length != tag.Length))
{
// Note that we don't allow 'W/' prefixes for weak ETags in the 'tag' parameter. If the user wants to
// add a weak ETag, he can set 'isWeak' to true.
throw new FormatException("Invalid ETag name");
}
_tag = tag;
_isWeak = isWeak;
}
public static EntityTagHeaderValue Any
{
get
{
if (AnyType == null)
{
AnyType = new EntityTagHeaderValue();
AnyType._tag = "*";
AnyType._isWeak = false;
}
return AnyType;
}
}
public StringSegment Tag
{
get { return _tag; }
}
public bool IsWeak
{
get { return _isWeak; }
}
public override string ToString()
{
if (_isWeak)
{
return "W/" + _tag.ToString();
}
return _tag.ToString();
}
/// <summary>
/// Check against another <see cref="EntityTagHeaderValue"/> for equality.
/// This equality check should not be used to determine if two values match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2).
/// </summary>
/// <param name="obj">The other value to check against for equality.</param>
/// <returns>
/// <c>true</c> if the strength and tag of the two values match,
/// <c>false</c> if the other value is null, is not an <see cref="EntityTagHeaderValue"/>, or if there is a mismatch of strength or tag between the two values.
/// </returns>
public override bool Equals(object obj)
{
var other = obj as EntityTagHeaderValue;
if (other == null)
{
return false;
}
// Since the tag is a quoted-string we treat it case-sensitive.
return _isWeak == other._isWeak && StringSegment.Equals(_tag, other._tag, StringComparison.Ordinal);
}
public override int GetHashCode()
{
// Since the tag is a quoted-string we treat it case-sensitive.
return _tag.GetHashCode() ^ _isWeak.GetHashCode();
}
/// <summary>
/// Compares against another <see cref="EntityTagHeaderValue"/> to see if they match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2).
/// </summary>
/// <param name="other">The other <see cref="EntityTagHeaderValue"/> to compare against.</param>
/// <param name="useStrongComparison"><c>true</c> to use a strong comparison, <c>false</c> to use a weak comparison</param>
/// <returns>
/// <c>true</c> if the <see cref="EntityTagHeaderValue"/> match for the given comparison type,
/// <c>false</c> if the other value is null or the comparison failed.
/// </returns>
public bool Compare(EntityTagHeaderValue other, bool useStrongComparison)
{
if (other == null)
{
return false;
}
if (useStrongComparison)
{
return !IsWeak && !other.IsWeak && StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal);
}
else
{
return StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal);
}
}
public static EntityTagHeaderValue Parse(StringSegment input)
{
var index = 0;
return SingleValueParser.ParseValue(input, ref index);
}
public static bool TryParse(StringSegment input, out EntityTagHeaderValue parsedValue)
{
var index = 0;
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
}
public static IList<EntityTagHeaderValue> ParseList(IList<string> inputs)
{
return MultipleValueParser.ParseValues(inputs);
}
public static IList<EntityTagHeaderValue> ParseStrictList(IList<string> inputs)
{
return MultipleValueParser.ParseStrictValues(inputs);
}
public static bool TryParseList(IList<string> inputs, out IList<EntityTagHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
}
public static bool TryParseStrictList(IList<string> inputs, out IList<EntityTagHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
}
internal static int GetEntityTagLength(StringSegment input, int startIndex, out EntityTagHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Caller must remove leading whitespaces. If not, we'll return 0.
var isWeak = false;
var current = startIndex;
var firstChar = input[startIndex];
if (firstChar == '*')
{
// We have '*' value, indicating "any" ETag.
parsedValue = Any;
current++;
}
else
{
// The RFC defines 'W/' as prefix, but we'll be flexible and also accept lower-case 'w'.
if ((firstChar == 'W') || (firstChar == 'w'))
{
current++;
// We need at least 3 more chars: the '/' character followed by two quotes.
if ((current + 2 >= input.Length) || (input[current] != '/'))
{
return 0;
}
isWeak = true;
current++; // we have a weak-entity tag.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
}
var tagStartIndex = current;
var tagLength = 0;
if (HttpRuleParser.GetQuotedStringLength(input, current, out tagLength) != HttpParseResult.Parsed)
{
return 0;
}
parsedValue = new EntityTagHeaderValue();
if (tagLength == input.Length)
{
// Most of the time we'll have strong ETags without leading/trailing whitespaces.
Contract.Assert(startIndex == 0);
Contract.Assert(!isWeak);
parsedValue._tag = input;
parsedValue._isWeak = false;
}
else
{
parsedValue._tag = input.Subsegment(tagStartIndex, tagLength);
parsedValue._isWeak = isWeak;
}
current = current + tagLength;
}
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
return current - startIndex;
}
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
internal sealed class GenericHeaderParser<T> : BaseHeaderParser<T>
{
internal delegate int GetParsedValueLengthDelegate(StringSegment value, int startIndex, out T parsedValue);
private GetParsedValueLengthDelegate _getParsedValueLength;
internal GenericHeaderParser(bool supportsMultipleValues, GetParsedValueLengthDelegate getParsedValueLength)
: base(supportsMultipleValues)
{
if (getParsedValueLength == null)
{
throw new ArgumentNullException(nameof(getParsedValueLength));
}
_getParsedValueLength = getParsedValueLength;
}
protected override int GetParsedValueLength(StringSegment value, int startIndex, out T parsedValue)
{
return _getParsedValueLength(value, startIndex, out parsedValue);
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.Net.Http.Headers
{
public static class HeaderNames
{
public const string Accept = "Accept";
public const string AcceptCharset = "Accept-Charset";
public const string AcceptEncoding = "Accept-Encoding";
public const string AcceptLanguage = "Accept-Language";
public const string AcceptRanges = "Accept-Ranges";
public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
public const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers";
public const string AccessControlMaxAge = "Access-Control-Max-Age";
public const string AccessControlRequestHeaders = "Access-Control-Request-Headers";
public const string AccessControlRequestMethod = "Access-Control-Request-Method";
public const string Age = "Age";
public const string Allow = "Allow";
public const string Authority = ":authority";
public const string Authorization = "Authorization";
public const string CacheControl = "Cache-Control";
public const string Connection = "Connection";
public const string ContentDisposition = "Content-Disposition";
public const string ContentEncoding = "Content-Encoding";
public const string ContentLanguage = "Content-Language";
public const string ContentLength = "Content-Length";
public const string ContentLocation = "Content-Location";
public const string ContentMD5 = "Content-MD5";
public const string ContentRange = "Content-Range";
public const string ContentSecurityPolicy = "Content-Security-Policy";
public const string ContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only";
public const string ContentType = "Content-Type";
public const string Cookie = "Cookie";
public const string Date = "Date";
public const string ETag = "ETag";
public const string Expires = "Expires";
public const string Expect = "Expect";
public const string From = "From";
public const string Host = "Host";
public const string IfMatch = "If-Match";
public const string IfModifiedSince = "If-Modified-Since";
public const string IfNoneMatch = "If-None-Match";
public const string IfRange = "If-Range";
public const string IfUnmodifiedSince = "If-Unmodified-Since";
public const string LastModified = "Last-Modified";
public const string Location = "Location";
public const string MaxForwards = "Max-Forwards";
public const string Method = ":method";
public const string Origin = "Origin";
public const string Path = ":path";
public const string Pragma = "Pragma";
public const string ProxyAuthenticate = "Proxy-Authenticate";
public const string ProxyAuthorization = "Proxy-Authorization";
public const string Range = "Range";
public const string Referer = "Referer";
public const string RetryAfter = "Retry-After";
public const string Scheme = ":scheme";
public const string Server = "Server";
public const string SetCookie = "Set-Cookie";
public const string Status = ":status";
public const string StrictTransportSecurity = "Strict-Transport-Security";
public const string TE = "TE";
public const string Trailer = "Trailer";
public const string TransferEncoding = "Transfer-Encoding";
public const string Upgrade = "Upgrade";
public const string UserAgent = "User-Agent";
public const string Vary = "Vary";
public const string Via = "Via";
public const string Warning = "Warning";
public const string WebSocketSubProtocols = "Sec-WebSocket-Protocol";
public const string WWWAuthenticate = "WWW-Authenticate";
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.Net.Http.Headers
{
public static class HeaderQuality
{
/// <summary>
/// Quality factor to indicate a perfect match.
/// </summary>
public const double Match = 1.0;
/// <summary>
/// Quality factor to indicate no match.
/// </summary>
public const double NoMatch = 0.0;
}
}

View File

@ -0,0 +1,732 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
public static class HeaderUtilities
{
private static readonly int _int64MaxStringLength = 19;
private static readonly int _qualityValueMaxCharCount = 10; // Little bit more permissive than RFC7231 5.3.1
private const string QualityName = "q";
internal const string BytesUnit = "bytes";
internal static void SetQuality(IList<NameValueHeaderValue> parameters, double? value)
{
Contract.Requires(parameters != null);
var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName);
if (value.HasValue)
{
// Note that even if we check the value here, we can't prevent a user from adding an invalid quality
// value using Parameters.Add(). Even if we would prevent the user from adding an invalid value
// using Parameters.Add() he could always add invalid values using HttpHeaders.AddWithoutValidation().
// So this check is really for convenience to show users that they're trying to add an invalid
// value.
if ((value < 0) || (value > 1))
{
throw new ArgumentOutOfRangeException(nameof(value));
}
var qualityString = ((double)value).ToString("0.0##", NumberFormatInfo.InvariantInfo);
if (qualityParameter != null)
{
qualityParameter.Value = qualityString;
}
else
{
parameters.Add(new NameValueHeaderValue(QualityName, qualityString));
}
}
else
{
// Remove quality parameter
if (qualityParameter != null)
{
parameters.Remove(qualityParameter);
}
}
}
internal static double? GetQuality(IList<NameValueHeaderValue> parameters)
{
Contract.Requires(parameters != null);
var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName);
if (qualityParameter != null)
{
// Note that the RFC requires decimal '.' regardless of the culture. I.e. using ',' as decimal
// separator is considered invalid (even if the current culture would allow it).
if (TryParseQualityDouble(qualityParameter.Value, 0, out var qualityValue, out var length))
{
return qualityValue;
}
}
return null;
}
internal static void CheckValidToken(StringSegment value, string parameterName)
{
if (StringSegment.IsNullOrEmpty(value))
{
throw new ArgumentException("An empty string is not allowed.", parameterName);
}
if (HttpRuleParser.GetTokenLength(value, 0) != value.Length)
{
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid token '{0}.", value));
}
}
internal static bool AreEqualCollections<T>(ICollection<T> x, ICollection<T> y)
{
return AreEqualCollections(x, y, null);
}
internal static bool AreEqualCollections<T>(ICollection<T> x, ICollection<T> y, IEqualityComparer<T> comparer)
{
if (x == null)
{
return (y == null) || (y.Count == 0);
}
if (y == null)
{
return (x.Count == 0);
}
if (x.Count != y.Count)
{
return false;
}
if (x.Count == 0)
{
return true;
}
// We have two unordered lists. So comparison is an O(n*m) operation which is expensive. Usually
// headers have 1-2 parameters (if any), so this comparison shouldn't be too expensive.
var alreadyFound = new bool[x.Count];
var i = 0;
foreach (var xItem in x)
{
Contract.Assert(xItem != null);
i = 0;
var found = false;
foreach (var yItem in y)
{
if (!alreadyFound[i])
{
if (((comparer == null) && xItem.Equals(yItem)) ||
((comparer != null) && comparer.Equals(xItem, yItem)))
{
alreadyFound[i] = true;
found = true;
break;
}
}
i++;
}
if (!found)
{
return false;
}
}
// Since we never re-use a "found" value in 'y', we expecte 'alreadyFound' to have all fields set to 'true'.
// Otherwise the two collections can't be equal and we should not get here.
Contract.Assert(Contract.ForAll(alreadyFound, value => { return value; }),
"Expected all values in 'alreadyFound' to be true since collections are considered equal.");
return true;
}
internal static int GetNextNonEmptyOrWhitespaceIndex(
StringSegment input,
int startIndex,
bool skipEmptyValues,
out bool separatorFound)
{
Contract.Requires(input != null);
Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length.
separatorFound = false;
var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
if ((current == input.Length) || (input[current] != ','))
{
return current;
}
// If we have a separator, skip the separator and all following whitespaces. If we support
// empty values, continue until the current character is neither a separator nor a whitespace.
separatorFound = true;
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
if (skipEmptyValues)
{
while ((current < input.Length) && (input[current] == ','))
{
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
}
}
return current;
}
private static int AdvanceCacheDirectiveIndex(int current, string headerValue)
{
// Skip until the next potential name
current += HttpRuleParser.GetWhitespaceLength(headerValue, current);
// Skip the value if present
if (current < headerValue.Length && headerValue[current] == '=')
{
current++; // skip '='
current += NameValueHeaderValue.GetValueLength(headerValue, current);
}
// Find the next delimiter
current = headerValue.IndexOf(',', current);
if (current == -1)
{
// If no delimiter found, skip to the end
return headerValue.Length;
}
current++; // skip ','
current += HttpRuleParser.GetWhitespaceLength(headerValue, current);
return current;
}
/// <summary>
/// Try to find a target header value among the set of given header values and parse it as a
/// <see cref="TimeSpan"/>.
/// </summary>
/// <param name="headerValues">
/// The <see cref="StringValues"/> containing the set of header values to search.
/// </param>
/// <param name="targetValue">
/// The target header value to look for.
/// </param>
/// <param name="value">
/// When this method returns, contains the parsed <see cref="TimeSpan"/>, if the parsing succeeded, or
/// null if the parsing failed. The conversion fails if the <paramref name="targetValue"/> was not
/// found or could not be parsed as a <see cref="TimeSpan"/>. This parameter is passed uninitialized;
/// any value originally supplied in result will be overwritten.
/// </param>
/// <returns>
/// <code>true</code> if <paramref name="targetValue"/> is found and successfully parsed; otherwise,
/// <code>false</code>.
/// </returns>
// e.g. { "headerValue=10, targetHeaderValue=30" }
public static bool TryParseSeconds(StringValues headerValues, string targetValue, out TimeSpan? value)
{
if (StringValues.IsNullOrEmpty(headerValues) || string.IsNullOrEmpty(targetValue))
{
value = null;
return false;
}
for (var i = 0; i < headerValues.Count; i++)
{
// Trim leading white space
var current = HttpRuleParser.GetWhitespaceLength(headerValues[i], 0);
while (current < headerValues[i].Length)
{
long seconds;
var initial = current;
var tokenLength = HttpRuleParser.GetTokenLength(headerValues[i], current);
if (tokenLength == targetValue.Length
&& string.Compare(headerValues[i], current, targetValue, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0
&& TryParseNonNegativeInt64FromHeaderValue(current + tokenLength, headerValues[i], out seconds))
{
// Token matches target value and seconds were parsed
value = TimeSpan.FromSeconds(seconds);
return true;
}
current = AdvanceCacheDirectiveIndex(current + tokenLength, headerValues[i]);
// Ensure index was advanced
if (current <= initial)
{
Debug.Assert(false, $"Index '{nameof(current)}' not advanced, this is a bug.");
value = null;
return false;
}
}
}
value = null;
return false;
}
/// <summary>
/// Check if a target directive exists among the set of given cache control directives.
/// </summary>
/// <param name="cacheControlDirectives">
/// The <see cref="StringValues"/> containing the set of cache control directives.
/// </param>
/// <param name="targetDirectives">
/// The target cache control directives to look for.
/// </param>
/// <returns>
/// <code>true</code> if <paramref name="targetDirectives"/> is contained in <paramref name="cacheControlDirectives"/>;
/// otherwise, <code>false</code>.
/// </returns>
public static bool ContainsCacheDirective(StringValues cacheControlDirectives, string targetDirectives)
{
if (StringValues.IsNullOrEmpty(cacheControlDirectives) || string.IsNullOrEmpty(targetDirectives))
{
return false;
}
for (var i = 0; i < cacheControlDirectives.Count; i++)
{
// Trim leading white space
var current = HttpRuleParser.GetWhitespaceLength(cacheControlDirectives[i], 0);
while (current < cacheControlDirectives[i].Length)
{
var initial = current;
var tokenLength = HttpRuleParser.GetTokenLength(cacheControlDirectives[i], current);
if (tokenLength == targetDirectives.Length
&& string.Compare(cacheControlDirectives[i], current, targetDirectives, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0)
{
// Token matches target value
return true;
}
current = AdvanceCacheDirectiveIndex(current + tokenLength, cacheControlDirectives[i]);
// Ensure index was advanced
if (current <= initial)
{
Debug.Assert(false, $"Index '{nameof(current)}' not advanced, this is a bug.");
return false;
}
}
}
return false;
}
private static unsafe bool TryParseNonNegativeInt64FromHeaderValue(int startIndex, string headerValue, out long result)
{
// Trim leading whitespace
startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex);
// Match and skip '=', it also can't be the last character in the headerValue
if (startIndex >= headerValue.Length - 1 || headerValue[startIndex] != '=')
{
result = 0;
return false;
}
startIndex++;
// Trim trailing whitespace
startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex);
// Try parse the number
if (TryParseNonNegativeInt64(new StringSegment(headerValue, startIndex, HttpRuleParser.GetNumberLength(headerValue, startIndex, false)), out result))
{
return true;
}
result = 0;
return false;
}
/// <summary>
/// Try to convert a string representation of a positive number to its 64-bit signed integer equivalent.
/// A return value indicates whether the conversion succeeded or failed.
/// </summary>
/// <param name="value">
/// A string containing a number to convert.
/// </param>
/// <param name="result">
/// When this method returns, contains the 64-bit signed integer value equivalent of the number contained
/// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if
/// the string is null or String.Empty, is not of the correct format, is negative, or represents a number
/// greater than Int64.MaxValue. This parameter is passed uninitialized; any value originally supplied in
/// result will be overwritten.
/// </param>
/// <returns><code>true</code> if parsing succeeded; otherwise, <code>false</code>.</returns>
public static unsafe bool TryParseNonNegativeInt32(StringSegment value, out int result)
{
if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0)
{
result = 0;
return false;
}
result = 0;
fixed (char* ptr = value.Buffer)
{
var ch = (ushort*)ptr + value.Offset;
var end = ch + value.Length;
ushort digit = 0;
while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9)
{
// Check for overflow
if ((result = result * 10 + digit) < 0)
{
result = 0;
return false;
}
ch++;
}
if (ch != end)
{
result = 0;
return false;
}
return true;
}
}
/// <summary>
/// Try to convert a <see cref="StringSegment"/> representation of a positive number to its 64-bit signed
/// integer equivalent. A return value indicates whether the conversion succeeded or failed.
/// </summary>
/// <param name="value">
/// A <see cref="StringSegment"/> containing a number to convert.
/// </param>
/// <param name="result">
/// When this method returns, contains the 64-bit signed integer value equivalent of the number contained
/// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if
/// the <see cref="StringSegment"/> is null or String.Empty, is not of the correct format, is negative, or
/// represents a number greater than Int64.MaxValue. This parameter is passed uninitialized; any value
/// originally supplied in result will be overwritten.
/// </param>
/// <returns><code>true</code> if parsing succeeded; otherwise, <code>false</code>.</returns>
public static unsafe bool TryParseNonNegativeInt64(StringSegment value, out long result)
{
if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0)
{
result = 0;
return false;
}
result = 0;
fixed (char* ptr = value.Buffer)
{
var ch = (ushort*)ptr + value.Offset;
var end = ch + value.Length;
ushort digit = 0;
while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9)
{
// Check for overflow
if ((result = result * 10 + digit) < 0)
{
result = 0;
return false;
}
ch++;
}
if (ch != end)
{
result = 0;
return false;
}
return true;
}
}
// Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation)
// See https://tools.ietf.org/html/rfc7231#section-5.3.1
// Check is made to verify if the value is between 0 and 1 (and it returns False if the check fails).
internal static bool TryParseQualityDouble(StringSegment input, int startIndex, out double quality, out int length)
{
quality = 0;
length = 0;
var inputLength = input.Length;
var current = startIndex;
var limit = startIndex + _qualityValueMaxCharCount;
var intPart = 0;
var decPart = 0;
var decPow = 1;
if (current >= inputLength)
{
return false;
}
var ch = input[current];
if (ch >= '0' && ch <= '1') // Only values between 0 and 1 are accepted, according to RFC
{
intPart = ch - '0';
current++;
}
else
{
// The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the
// form "0.123".
return false;
}
if (current < inputLength)
{
ch = input[current];
if (ch >= '0' && ch <= '9')
{
// The RFC accepts only one digit before the dot
return false;
}
if (ch == '.')
{
current++;
while (current < inputLength)
{
ch = input[current];
if (ch >= '0' && ch <= '9')
{
if (current >= limit)
{
return false;
}
decPart = decPart * 10 + ch - '0';
decPow *= 10;
current++;
}
else
{
break;
}
}
}
}
if (decPart != 0)
{
quality = intPart + decPart / (double)decPow;
}
else
{
quality = intPart;
}
if (quality > 1)
{
// reset quality
quality = 0;
return false;
}
length = current - startIndex;
return true;
}
/// <summary>
/// Converts the non-negative 64-bit numeric value to its equivalent string representation.
/// </summary>
/// <param name="value">
/// The number to convert.
/// </param>
/// <returns>
/// The string representation of the value of this instance, consisting of a sequence of digits ranging from 0 to 9 with no leading zeroes.
/// </returns>
public unsafe static string FormatNonNegativeInt64(long value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "The value to be formatted must be non-negative.");
}
var position = _int64MaxStringLength;
char* charBuffer = stackalloc char[_int64MaxStringLength];
do
{
// Consider using Math.DivRem() if available
var quotient = value / 10;
charBuffer[--position] = (char)(0x30 + (value - quotient * 10)); // 0x30 = '0'
value = quotient;
}
while (value != 0);
return new string(charBuffer, position, _int64MaxStringLength - position);
}
public static bool TryParseDate(StringSegment input, out DateTimeOffset result)
{
return HttpRuleParser.TryStringToDate(input, out result);
}
public static string FormatDate(DateTimeOffset dateTime)
{
return FormatDate(dateTime, false);
}
public static string FormatDate(DateTimeOffset dateTime, bool quoted)
{
return dateTime.ToRfc1123String(quoted);
}
public static StringSegment RemoveQuotes(StringSegment input)
{
if (IsQuoted(input))
{
input = input.Subsegment(1, input.Length - 2);
}
return input;
}
public static bool IsQuoted(StringSegment input)
{
return !StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"';
}
/// <summary>
/// Given a quoted-string as defined by <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>,
/// removes quotes and unescapes backslashes and quotes. This assumes that the input is a valid quoted-string.
/// </summary>
/// <param name="input">The quoted-string to be unescaped.</param>
/// <returns>An unescaped version of the quoted-string.</returns>
public static StringSegment UnescapeAsQuotedString(StringSegment input)
{
input = RemoveQuotes(input);
// First pass to calculate the size of the InplaceStringBuilder
var backSlashCount = CountBackslashesForDecodingQuotedString(input);
if (backSlashCount == 0)
{
return input;
}
var stringBuilder = new InplaceStringBuilder(input.Length - backSlashCount);
for (var i = 0; i < input.Length; i++)
{
if (i < input.Length - 1 && input[i] == '\\')
{
// If there is an backslash character as the last character in the string,
// we will assume that it should be included literally in the unescaped string
// Ex: "hello\\" => "hello\\"
// Also, if a sender adds a quoted pair like '\\''n',
// we will assume it is over escaping and just add a n to the string.
// Ex: "he\\llo" => "hello"
stringBuilder.Append(input[i + 1]);
i++;
continue;
}
stringBuilder.Append(input[i]);
}
return stringBuilder.ToString();
}
private static int CountBackslashesForDecodingQuotedString(StringSegment input)
{
var numberBackSlashes = 0;
for (var i = 0; i < input.Length; i++)
{
if (i < input.Length - 1 && input[i] == '\\')
{
// If there is an backslash character as the last character in the string,
// we will assume that it should be included literally in the unescaped string
// Ex: "hello\\" => "hello\\"
// Also, if a sender adds a quoted pair like '\\''n',
// we will assume it is over escaping and just add a n to the string.
// Ex: "he\\llo" => "hello"
if (input[i + 1] == '\\')
{
// Only count escaped backslashes once
i++;
}
numberBackSlashes++;
}
}
return numberBackSlashes;
}
/// <summary>
/// Escapes a <see cref="StringSegment"/> as a quoted-string, which is defined by
/// <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>.
/// </summary>
/// <remarks>
/// This will add a backslash before each backslash and quote and add quotes
/// around the input. Assumes that the input does not have quotes around it,
/// as this method will add them. Throws if the input contains any invalid escape characters,
/// as defined by rfc7230.
/// </remarks>
/// <param name="input">The input to be escaped.</param>
/// <returns>An escaped version of the quoted-string.</returns>
public static StringSegment EscapeAsQuotedString(StringSegment input)
{
// By calling this, we know that the string requires quotes around it to be a valid token.
var backSlashCount = CountAndCheckCharactersNeedingBackslashesWhenEncoding(input);
var stringBuilder = new InplaceStringBuilder(input.Length + backSlashCount + 2); // 2 for quotes
stringBuilder.Append('\"');
for (var i = 0; i < input.Length; i++)
{
if (input[i] == '\\' || input[i] == '\"')
{
stringBuilder.Append('\\');
}
else if ((input[i] <= 0x1F || input[i] == 0x7F) && input[i] != 0x09)
{
// Control characters are not allowed in a quoted-string, which include all characters
// below 0x1F (except for 0x09 (TAB)) and 0x7F.
throw new FormatException($"Invalid control character '{input[i]}' in input.");
}
stringBuilder.Append(input[i]);
}
stringBuilder.Append('\"');
return stringBuilder.ToString();
}
private static int CountAndCheckCharactersNeedingBackslashesWhenEncoding(StringSegment input)
{
var numberOfCharactersNeedingEscaping = 0;
for (var i = 0; i < input.Length; i++)
{
if (input[i] == '\\' || input[i] == '\"')
{
numberOfCharactersNeedingEscaping++;
}
}
return numberOfCharactersNeedingEscaping;
}
internal static void ThrowIfReadOnly(bool isReadOnly)
{
if (isReadOnly)
{
throw new InvalidOperationException("The object cannot be modified because it is read-only.");
}
}
}
}

View File

@ -0,0 +1,172 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
internal abstract class HttpHeaderParser<T>
{
private bool _supportsMultipleValues;
protected HttpHeaderParser(bool supportsMultipleValues)
{
_supportsMultipleValues = supportsMultipleValues;
}
public bool SupportsMultipleValues
{
get { return _supportsMultipleValues; }
}
// If a parser supports multiple values, a call to ParseValue/TryParseValue should return a value for 'index'
// pointing to the next non-whitespace character after a delimiter. E.g. if called with a start index of 0
// for string "value , second_value", then after the call completes, 'index' must point to 's', i.e. the first
// non-whitespace after the separator ','.
public abstract bool TryParseValue(StringSegment value, ref int index, out T parsedValue);
public T ParseValue(StringSegment value, ref int index)
{
// Index may be value.Length (e.g. both 0). This may be allowed for some headers (e.g. Accept but not
// allowed by others (e.g. Content-Length). The parser has to decide if this is valid or not.
Contract.Requires((value == null) || ((index >= 0) && (index <= value.Length)));
// If a parser returns 'null', it means there was no value, but that's valid (e.g. "Accept: "). The caller
// can ignore the value.
T result;
if (!TryParseValue(value, ref index, out result))
{
throw new FormatException(string.Format(CultureInfo.InvariantCulture,
"The header contains invalid values at index {0}: '{1}'", index, value.Value ?? "<null>"));
}
return result;
}
public virtual bool TryParseValues(IList<string> values, out IList<T> parsedValues)
{
return TryParseValues(values, strict: false, parsedValues: out parsedValues);
}
public virtual bool TryParseStrictValues(IList<string> values, out IList<T> parsedValues)
{
return TryParseValues(values, strict: true, parsedValues: out parsedValues);
}
protected virtual bool TryParseValues(IList<string> values, bool strict, out IList<T> parsedValues)
{
Contract.Assert(_supportsMultipleValues);
// If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller
// can ignore the value.
parsedValues = null;
List<T> results = null;
if (values == null)
{
return false;
}
for (var i = 0; i < values.Count; i++)
{
var value = values[i];
int index = 0;
while (!string.IsNullOrEmpty(value) && index < value.Length)
{
T output;
if (TryParseValue(value, ref index, out output))
{
// The entry may not contain an actual value, like " , "
if (output != null)
{
if (results == null)
{
results = new List<T>(); // Allocate it only when used
}
results.Add(output);
}
}
else if (strict)
{
return false;
}
else
{
// Skip the invalid values and keep trying.
index++;
}
}
}
if (results != null)
{
parsedValues = results;
return true;
}
return false;
}
public virtual IList<T> ParseValues(IList<string> values)
{
return ParseValues(values, strict: false);
}
public virtual IList<T> ParseStrictValues(IList<string> values)
{
return ParseValues(values, strict: true);
}
protected virtual IList<T> ParseValues(IList<string> values, bool strict)
{
Contract.Assert(_supportsMultipleValues);
// If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller
// can ignore the value.
var parsedValues = new List<T>();
if (values == null)
{
return parsedValues;
}
foreach (var value in values)
{
int index = 0;
while (!string.IsNullOrEmpty(value) && index < value.Length)
{
T output;
if (TryParseValue(value, ref index, out output))
{
// The entry may not contain an actual value, like " , "
if (output != null)
{
parsedValues.Add(output);
}
}
else if (strict)
{
throw new FormatException(string.Format(CultureInfo.InvariantCulture,
"The header contains invalid values at index {0}: '{1}'", index, value));
}
else
{
// Skip the invalid values and keep trying.
index++;
}
}
}
return parsedValues;
}
// If ValueType is a custom header value type (e.g. NameValueHeaderValue) it implements ToString() correctly.
// However for existing types like int, byte[], DateTimeOffset we can't override ToString(). Therefore the
// parser provides a ToString() virtual method that can be overridden by derived types to correctly serialize
// values (e.g. byte[] to Base64 encoded string).
// The default implementation is to just call ToString() on the value itself which is the right thing to do
// for most headers (custom types, string, etc.).
public virtual string ToString(object value)
{
Contract.Requires(value != null);
return value.ToString();
}
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.Net.Http.Headers
{
internal enum HttpParseResult
{
Parsed,
NotParsed,
InvalidFormat,
}
}

View File

@ -0,0 +1,349 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
internal static class HttpRuleParser
{
private static readonly bool[] TokenChars = CreateTokenChars();
private const int MaxNestedCount = 5;
private static readonly string[] DateFormats = new string[] {
// "r", // RFC 1123, required output format but too strict for input
"ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time)
"ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT
"d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week
"d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone
"ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year
"ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone
"d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year
"d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone
"dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850, short year
"dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone
"ddd, d'-'MMM'-'yyyy H:m:s 'GMT'", // RFC 850, long year
"ddd MMM d H:m:s yyyy", // ANSI C's asctime() format
"ddd, d MMM yyyy H:m:s zzz", // RFC 5322
"ddd, d MMM yyyy H:m:s", // RFC 5322 no zone
"d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week
"d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone
};
internal const char CR = '\r';
internal const char LF = '\n';
internal const char SP = ' ';
internal const char Tab = '\t';
internal const int MaxInt64Digits = 19;
internal const int MaxInt32Digits = 10;
// iso-8859-1, Western European (ISO)
internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding("iso-8859-1");
private static bool[] CreateTokenChars()
{
// token = 1*<any CHAR except CTLs or separators>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
var tokenChars = new bool[128]; // everything is false
for (int i = 33; i < 127; i++) // skip Space (32) & DEL (127)
{
tokenChars[i] = true;
}
// remove separators: these are not valid token characters
tokenChars[(byte)'('] = false;
tokenChars[(byte)')'] = false;
tokenChars[(byte)'<'] = false;
tokenChars[(byte)'>'] = false;
tokenChars[(byte)'@'] = false;
tokenChars[(byte)','] = false;
tokenChars[(byte)';'] = false;
tokenChars[(byte)':'] = false;
tokenChars[(byte)'\\'] = false;
tokenChars[(byte)'"'] = false;
tokenChars[(byte)'/'] = false;
tokenChars[(byte)'['] = false;
tokenChars[(byte)']'] = false;
tokenChars[(byte)'?'] = false;
tokenChars[(byte)'='] = false;
tokenChars[(byte)'{'] = false;
tokenChars[(byte)'}'] = false;
return tokenChars;
}
internal static bool IsTokenChar(char character)
{
// Must be between 'space' (32) and 'DEL' (127)
if (character > 127)
{
return false;
}
return TokenChars[character];
}
[Pure]
internal static int GetTokenLength(StringSegment input, int startIndex)
{
Contract.Requires(input != null);
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
if (startIndex >= input.Length)
{
return 0;
}
var current = startIndex;
while (current < input.Length)
{
if (!IsTokenChar(input[current]))
{
return current - startIndex;
}
current++;
}
return input.Length - startIndex;
}
internal static int GetWhitespaceLength(StringSegment input, int startIndex)
{
Contract.Requires(input != null);
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
if (startIndex >= input.Length)
{
return 0;
}
var current = startIndex;
char c;
while (current < input.Length)
{
c = input[current];
if ((c == SP) || (c == Tab))
{
current++;
continue;
}
if (c == CR)
{
// If we have a #13 char, it must be followed by #10 and then at least one SP or HT.
if ((current + 2 < input.Length) && (input[current + 1] == LF))
{
char spaceOrTab = input[current + 2];
if ((spaceOrTab == SP) || (spaceOrTab == Tab))
{
current += 3;
continue;
}
}
}
return current - startIndex;
}
// All characters between startIndex and the end of the string are LWS characters.
return input.Length - startIndex;
}
internal static int GetNumberLength(StringSegment input, int startIndex, bool allowDecimal)
{
Contract.Requires(input != null);
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
var current = startIndex;
char c;
// If decimal values are not allowed, we pretend to have read the '.' character already. I.e. if a dot is
// found in the string, parsing will be aborted.
var haveDot = !allowDecimal;
// The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the
// form "0.123". Also, there are no negative values defined in the RFC. So we'll just parse non-negative
// values.
// The RFC only allows decimal dots not ',' characters as decimal separators. Therefore value "1,23" is
// considered invalid and must be represented as "1.23".
if (input[current] == '.')
{
return 0;
}
while (current < input.Length)
{
c = input[current];
if ((c >= '0') && (c <= '9'))
{
current++;
}
else if (!haveDot && (c == '.'))
{
// Note that value "1." is valid.
haveDot = true;
current++;
}
else
{
break;
}
}
return current - startIndex;
}
internal static HttpParseResult GetQuotedStringLength(StringSegment input, int startIndex, out int length)
{
var nestedCount = 0;
return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length);
}
// quoted-pair = "\" CHAR
// CHAR = <any US-ASCII character (octets 0 - 127)>
internal static HttpParseResult GetQuotedPairLength(StringSegment input, int startIndex, out int length)
{
Contract.Requires(input != null);
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
Contract.Ensures((Contract.ValueAtReturn(out length) >= 0) &&
(Contract.ValueAtReturn(out length) <= (input.Length - startIndex)));
length = 0;
if (input[startIndex] != '\\')
{
return HttpParseResult.NotParsed;
}
// Quoted-char has 2 characters. Check wheter there are 2 chars left ('\' + char)
// If so, check whether the character is in the range 0-127. If not, it's an invalid value.
if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127))
{
return HttpParseResult.InvalidFormat;
}
// We don't care what the char next to '\' is.
length = 2;
return HttpParseResult.Parsed;
}
// Try the various date formats in the order listed above.
// We should accept a wide verity of common formats, but only output RFC 1123 style dates.
internal static bool TryStringToDate(StringSegment input, out DateTimeOffset result) =>
DateTimeOffset.TryParseExact(input.ToString(), DateFormats, DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result);
// TEXT = <any OCTET except CTLs, but including LWS>
// LWS = [CRLF] 1*( SP | HT )
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
//
// Since we don't really care about the content of a quoted string or comment, we're more tolerant and
// allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment).
//
// 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like
// "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested
// comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any)
// is unusual.
private static HttpParseResult GetExpressionLength(
StringSegment input,
int startIndex,
char openChar,
char closeChar,
bool supportsNesting,
ref int nestedCount,
out int length)
{
Contract.Requires(input != null);
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
Contract.Ensures((Contract.Result<HttpParseResult>() != HttpParseResult.Parsed) ||
(Contract.ValueAtReturn<int>(out length) > 0));
length = 0;
if (input[startIndex] != openChar)
{
return HttpParseResult.NotParsed;
}
var current = startIndex + 1; // Start parsing with the character next to the first open-char
while (current < input.Length)
{
// Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e.
// quoted char + closing char). Otherwise the closing char may be considered part of the quoted char.
var quotedPairLength = 0;
if ((current + 2 < input.Length) &&
(GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed))
{
// We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair,
// but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only
// allows ASCII chars after '\'; qdtext allows both '\' and >127 chars).
current = current + quotedPairLength;
continue;
}
// If we support nested expressions and we find an open-char, then parse the nested expressions.
if (supportsNesting && (input[current] == openChar))
{
nestedCount++;
try
{
// Check if we exceeded the number of nested calls.
if (nestedCount > MaxNestedCount)
{
return HttpParseResult.InvalidFormat;
}
var nestedLength = 0;
HttpParseResult nestedResult = GetExpressionLength(input, current, openChar, closeChar,
supportsNesting, ref nestedCount, out nestedLength);
switch (nestedResult)
{
case HttpParseResult.Parsed:
current += nestedLength; // add the length of the nested expression and continue.
break;
case HttpParseResult.NotParsed:
Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression " +
"parsing, because we found the open-char. So either it's a valid nested " +
"expression or it has invalid format.");
break;
case HttpParseResult.InvalidFormat:
// If the nested expression is invalid, we can't continue, so we fail with invalid format.
return HttpParseResult.InvalidFormat;
default:
Contract.Assert(false, "Unknown enum result: " + nestedResult);
break;
}
}
finally
{
nestedCount--;
}
}
if (input[current] == closeChar)
{
length = current - startIndex + 1;
return HttpParseResult.Parsed;
}
current++;
}
// We didn't see the final quote, therefore we have an invalid expression string.
return HttpParseResult.InvalidFormat;
}
}
}

View File

@ -0,0 +1,721 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
/// <summary>
/// Representation of the media type header. See <see href="https://tools.ietf.org/html/rfc6838"/>.
/// </summary>
public class MediaTypeHeaderValue
{
private const string BoundaryString = "boundary";
private const string CharsetString = "charset";
private const string MatchesAllString = "*/*";
private const string QualityString = "q";
private const string WildcardString = "*";
private const char ForwardSlashCharacter = '/';
private const char PeriodCharacter = '.';
private const char PlusCharacter = '+';
private static readonly char[] PeriodCharacterArray = new char[] { PeriodCharacter };
private static readonly HttpHeaderParser<MediaTypeHeaderValue> SingleValueParser
= new GenericHeaderParser<MediaTypeHeaderValue>(false, GetMediaTypeLength);
private static readonly HttpHeaderParser<MediaTypeHeaderValue> MultipleValueParser
= new GenericHeaderParser<MediaTypeHeaderValue>(true, GetMediaTypeLength);
// Use a collection instead of a dictionary since we may have multiple parameters with the same name.
private ObjectCollection<NameValueHeaderValue> _parameters;
private StringSegment _mediaType;
private bool _isReadOnly;
private MediaTypeHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
/// <summary>
/// Initializes a <see cref="MediaTypeHeaderValue"/> instance.
/// </summary>
/// <param name="mediaType">A <see cref="StringSegment"/> representation of a media type.
/// The text provided must be a single media type without parameters. </param>
public MediaTypeHeaderValue(StringSegment mediaType)
{
CheckMediaTypeFormat(mediaType, nameof(mediaType));
_mediaType = mediaType;
}
/// <summary>
/// Initializes a <see cref="MediaTypeHeaderValue"/> instance.
/// </summary>
/// <param name="mediaType">A <see cref="StringSegment"/> representation of a media type.
/// The text provided must be a single media type without parameters. </param>
/// <param name="quality">The <see cref="double"/> with the quality of the media type.</param>
public MediaTypeHeaderValue(StringSegment mediaType, double quality)
: this(mediaType)
{
Quality = quality;
}
/// <summary>
/// Gets or sets the value of the charset parameter. Returns <see cref="StringSegment.Empty"/>
/// if there is no charset.
/// </summary>
public StringSegment Charset
{
get
{
return NameValueHeaderValue.Find(_parameters, CharsetString)?.Value.Value;
}
set
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
// We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from
// setting a non-existing charset.
var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString);
if (StringSegment.IsNullOrEmpty(value))
{
// Remove charset parameter
if (charsetParameter != null)
{
Parameters.Remove(charsetParameter);
}
}
else
{
if (charsetParameter != null)
{
charsetParameter.Value = value;
}
else
{
Parameters.Add(new NameValueHeaderValue(CharsetString, value));
}
}
}
}
/// <summary>
/// Gets or sets the value of the Encoding parameter. Setting the Encoding will set
/// the <see cref="Charset"/> to <see cref="Encoding.WebName"/>.
/// </summary>
public Encoding Encoding
{
get
{
var charset = Charset;
if (!StringSegment.IsNullOrEmpty(charset))
{
try
{
return Encoding.GetEncoding(charset.Value);
}
catch (ArgumentException)
{
// Invalid or not supported
}
}
return null;
}
set
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
if (value == null)
{
Charset = null;
}
else
{
Charset = value.WebName;
}
}
}
/// <summary>
/// Gets or sets the value of the boundary parameter. Returns <see cref="StringSegment.Empty"/>
/// if there is no boundary.
/// </summary>
public StringSegment Boundary
{
get
{
return NameValueHeaderValue.Find(_parameters, BoundaryString)?.Value ?? default(StringSegment);
}
set
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString);
if (StringSegment.IsNullOrEmpty(value))
{
// Remove charset parameter
if (boundaryParameter != null)
{
Parameters.Remove(boundaryParameter);
}
}
else
{
if (boundaryParameter != null)
{
boundaryParameter.Value = value;
}
else
{
Parameters.Add(new NameValueHeaderValue(BoundaryString, value));
}
}
}
}
/// <summary>
/// Gets or sets the media type's parameters. Returns an empty <see cref="IList{T}"/>
/// if there are no parameters.
/// </summary>
public IList<NameValueHeaderValue> Parameters
{
get
{
if (_parameters == null)
{
if (IsReadOnly)
{
_parameters = ObjectCollection<NameValueHeaderValue>.EmptyReadOnlyCollection;
}
else
{
_parameters = new ObjectCollection<NameValueHeaderValue>();
}
}
return _parameters;
}
}
/// <summary>
/// Gets or sets the value of the quality parameter. Returns null
/// if there is no quality.
/// </summary>
public double? Quality
{
get { return HeaderUtilities.GetQuality(_parameters); }
set
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
HeaderUtilities.SetQuality(Parameters, value);
}
}
/// <summary>
/// Gets or sets the value of the media type. Returns <see cref="StringSegment.Empty"/>
/// if there is no media type.
/// </summary>
/// <example>
/// For the media type <c>"application/json"</c>, the property gives the value
/// <c>"application/json"</c>.
/// </example>
public StringSegment MediaType
{
get { return _mediaType; }
set
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
CheckMediaTypeFormat(value, nameof(value));
_mediaType = value;
}
}
/// <summary>
/// Gets the type of the <see cref="MediaTypeHeaderValue"/>.
/// </summary>
/// <example>
/// For the media type <c>"application/json"</c>, the property gives the value <c>"application"</c>.
/// </example>
/// <remarks>See <see href="https://tools.ietf.org/html/rfc6838#section-4.2"/> for more details on the type.</remarks>
public StringSegment Type
{
get
{
return _mediaType.Subsegment(0, _mediaType.IndexOf(ForwardSlashCharacter));
}
}
/// <summary>
/// Gets the subtype of the <see cref="MediaTypeHeaderValue"/>.
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"vnd.example+json"</c>.
/// </example>
/// <remarks>See <see href="https://tools.ietf.org/html/rfc6838#section-4.2"/> for more details on the subtype.</remarks>
public StringSegment SubType
{
get
{
return _mediaType.Subsegment(_mediaType.IndexOf(ForwardSlashCharacter) + 1);
}
}
/// <summary>
/// Gets subtype of the <see cref="MediaTypeHeaderValue"/>, excluding any structured syntax suffix. Returns <see cref="StringSegment.Empty"/>
/// if there is no subtype without suffix.
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"vnd.example"</c>.
/// </example>
public StringSegment SubTypeWithoutSuffix
{
get
{
var subType = SubType;
var startOfSuffix = subType.LastIndexOf(PlusCharacter);
if (startOfSuffix == -1)
{
return subType;
}
else
{
return subType.Subsegment(0, startOfSuffix);
}
}
}
/// <summary>
/// Gets the structured syntax suffix of the <see cref="MediaTypeHeaderValue"/> if it has one.
/// See <see href="https://tools.ietf.org/html/rfc6838#section-4.8">The RFC documentation on structured syntaxes.</see>
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"json"</c>.
/// </example>
public StringSegment Suffix
{
get
{
var subType = SubType;
var startOfSuffix = subType.LastIndexOf(PlusCharacter);
if (startOfSuffix == -1)
{
return default(StringSegment);
}
else
{
return subType.Subsegment(startOfSuffix + 1);
}
}
}
/// <summary>
/// Get a <see cref="IList{T}"/> of facets of the <see cref="MediaTypeHeaderValue"/>. Facets are a
/// period separated list of StringSegments in the <see cref="SubTypeWithoutSuffix"/>.
/// See <see href="https://tools.ietf.org/html/rfc6838#section-3">The RFC documentation on facets.</see>
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value:
/// <c>{"vnd", "example"}</c>
/// </example>
public IEnumerable<StringSegment> Facets
{
get
{
return SubTypeWithoutSuffix.Split(PeriodCharacterArray);
}
}
/// <summary>
/// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all types.
/// </summary>
public bool MatchesAllTypes => MediaType.Equals(MatchesAllString, StringComparison.Ordinal);
/// <summary>
/// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all subtypes.
/// </summary>
/// <example>
/// For the media type <c>"application/*"</c>, this property is <c>true</c>.
/// </example>
/// <example>
/// For the media type <c>"application/json"</c>, this property is <c>false</c>.
/// </example>
public bool MatchesAllSubTypes => SubType.Equals(WildcardString, StringComparison.Ordinal);
/// <summary>
/// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all subtypes, ignoring any structured syntax suffix.
/// </summary>
/// <example>
/// For the media type <c>"application/*+json"</c>, this property is <c>true</c>.
/// </example>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, this property is <c>false</c>.
/// </example>
public bool MatchesAllSubTypesWithoutSuffix =>
SubTypeWithoutSuffix.Equals(WildcardString, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets whether the <see cref="MediaTypeHeaderValue"/> is readonly.
/// </summary>
public bool IsReadOnly
{
get { return _isReadOnly; }
}
/// <summary>
/// Gets a value indicating whether this <see cref="MediaTypeHeaderValue"/> is a subset of
/// <paramref name="otherMediaType"/>. A "subset" is defined as the same or a more specific media type
/// according to the precedence described in https://www.ietf.org/rfc/rfc2068.txt section 14.1, Accept.
/// </summary>
/// <param name="otherMediaType">The <see cref="MediaTypeHeaderValue"/> to compare.</param>
/// <returns>
/// A value indicating whether this <see cref="MediaTypeHeaderValue"/> is a subset of
/// <paramref name="otherMediaType"/>.
/// </returns>
/// <remarks>
/// For example "multipart/mixed; boundary=1234" is a subset of "multipart/mixed; boundary=1234",
/// "multipart/mixed", "multipart/*", and "*/*" but not "multipart/mixed; boundary=2345" or
/// "multipart/message; boundary=1234".
/// </remarks>
public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType)
{
if (otherMediaType == null)
{
return false;
}
// "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*".
return MatchesType(otherMediaType) &&
MatchesSubtype(otherMediaType) &&
MatchesParameters(otherMediaType);
}
/// <summary>
/// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components,
/// while avoiding the cost of re-validating the components.
/// </summary>
/// <returns>A deep copy.</returns>
public MediaTypeHeaderValue Copy()
{
var other = new MediaTypeHeaderValue();
other._mediaType = _mediaType;
if (_parameters != null)
{
other._parameters = new ObjectCollection<NameValueHeaderValue>(
_parameters.Select(item => item.Copy()));
}
return other;
}
/// <summary>
/// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components,
/// while avoiding the cost of re-validating the components. This copy is read-only.
/// </summary>
/// <returns>A deep, read-only, copy.</returns>
public MediaTypeHeaderValue CopyAsReadOnly()
{
if (IsReadOnly)
{
return this;
}
var other = new MediaTypeHeaderValue();
other._mediaType = _mediaType;
if (_parameters != null)
{
other._parameters = new ObjectCollection<NameValueHeaderValue>(
_parameters.Select(item => item.CopyAsReadOnly()), isReadOnly: true);
}
other._isReadOnly = true;
return other;
}
public override string ToString()
{
var builder = new StringBuilder();
builder.Append(_mediaType);
NameValueHeaderValue.ToString(_parameters, separator: ';', leadingSeparator: true, destination: builder);
return builder.ToString();
}
public override bool Equals(object obj)
{
var other = obj as MediaTypeHeaderValue;
if (other == null)
{
return false;
}
return _mediaType.Equals(other._mediaType, StringComparison.OrdinalIgnoreCase) &&
HeaderUtilities.AreEqualCollections(_parameters, other._parameters);
}
public override int GetHashCode()
{
// The media-type string is case-insensitive.
return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_parameters);
}
/// <summary>
/// Takes a media type and parses it into the <see cref="MediaTypeHeaderValue" /> and its associated parameters.
/// </summary>
/// <param name="input">The <see cref="StringSegment"/> with the media type.</param>
/// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns>
public static MediaTypeHeaderValue Parse(StringSegment input)
{
var index = 0;
return SingleValueParser.ParseValue(input, ref index);
}
/// <summary>
/// Takes a media type, which can include parameters, and parses it into the <see cref="MediaTypeHeaderValue" /> and its associated parameters.
/// </summary>
/// <param name="input">The <see cref="StringSegment"/> with the media type. The media type constructed here must not have an y</param>
/// <param name="parsedValue">The parsed <see cref="MediaTypeHeaderValue"/></param>
/// <returns>True if the value was successfully parsed.</returns>
public static bool TryParse(StringSegment input, out MediaTypeHeaderValue parsedValue)
{
var index = 0;
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
}
/// <summary>
/// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters.
/// </summary>
/// <param name="inputs">A list of media types</param>
/// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns>
public static IList<MediaTypeHeaderValue> ParseList(IList<string> inputs)
{
return MultipleValueParser.ParseValues(inputs);
}
/// <summary>
/// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters.
/// Throws if there is invalid data in a string.
/// </summary>
/// <param name="inputs">A list of media types</param>
/// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns>
public static IList<MediaTypeHeaderValue> ParseStrictList(IList<string> inputs)
{
return MultipleValueParser.ParseStrictValues(inputs);
}
/// <summary>
/// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters.
/// </summary>
/// <param name="inputs">A list of media types</param>
/// <param name="parsedValues">The parsed <see cref="MediaTypeHeaderValue"/>.</param>
/// <returns>True if the value was successfully parsed.</returns>
public static bool TryParseList(IList<string> inputs, out IList<MediaTypeHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
}
/// <summary>
/// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters.
/// </summary>
/// <param name="inputs">A list of media types</param>
/// <param name="parsedValues">The parsed <see cref="MediaTypeHeaderValue"/>.</param>
/// <returns>True if the value was successfully parsed.</returns>
public static bool TryParseStrictList(IList<string> inputs, out IList<MediaTypeHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
}
private static int GetMediaTypeLength(StringSegment input, int startIndex, out MediaTypeHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Caller must remove leading whitespace. If not, we'll return 0.
var mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out var mediaType);
if (mediaTypeLength == 0)
{
return 0;
}
var current = startIndex + mediaTypeLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
MediaTypeHeaderValue mediaTypeHeader = null;
// If we're not done and we have a parameter delimiter, then we have a list of parameters.
if ((current < input.Length) && (input[current] == ';'))
{
mediaTypeHeader = new MediaTypeHeaderValue();
mediaTypeHeader._mediaType = mediaType;
current++; // skip delimiter.
var parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';',
mediaTypeHeader.Parameters);
parsedValue = mediaTypeHeader;
return current + parameterLength - startIndex;
}
// We have a media type without parameters.
mediaTypeHeader = new MediaTypeHeaderValue();
mediaTypeHeader._mediaType = mediaType;
parsedValue = mediaTypeHeader;
return current - startIndex;
}
private static int GetMediaTypeExpressionLength(StringSegment input, int startIndex, out StringSegment mediaType)
{
Contract.Requires((input != null) && (input.Length > 0) && (startIndex < input.Length));
// This method just parses the "type/subtype" string, it does not parse parameters.
mediaType = null;
// Parse the type, i.e. <type> in media type string "<type>/<subtype>; param1=value1; param2=value2"
var typeLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (typeLength == 0)
{
return 0;
}
var current = startIndex + typeLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
// Parse the separator between type and subtype
if ((current >= input.Length) || (input[current] != '/'))
{
return 0;
}
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
// Parse the subtype, i.e. <subtype> in media type string "<type>/<subtype>; param1=value1; param2=value2"
var subtypeLength = HttpRuleParser.GetTokenLength(input, current);
if (subtypeLength == 0)
{
return 0;
}
// If there is no whitespace between <type> and <subtype> in <type>/<subtype> get the media type using
// one Substring call. Otherwise get substrings for <type> and <subtype> and combine them.
var mediaTypeLength = current + subtypeLength - startIndex;
if (typeLength + subtypeLength + 1 == mediaTypeLength)
{
mediaType = input.Subsegment(startIndex, mediaTypeLength);
}
else
{
mediaType = input.Substring(startIndex, typeLength) + ForwardSlashCharacter + input.Substring(current, subtypeLength);
}
return mediaTypeLength;
}
private static void CheckMediaTypeFormat(StringSegment mediaType, string parameterName)
{
if (StringSegment.IsNullOrEmpty(mediaType))
{
throw new ArgumentException("An empty string is not allowed.", parameterName);
}
// When adding values using strongly typed objects, no leading/trailing LWS (whitespace) is allowed.
// Also no LWS between type and subtype is allowed.
var mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out var tempMediaType);
if ((mediaTypeLength == 0) || (tempMediaType.Length != mediaType.Length))
{
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid media type '{0}'.", mediaType));
}
}
private bool MatchesType(MediaTypeHeaderValue set)
{
return set.MatchesAllTypes ||
set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase);
}
private bool MatchesSubtype(MediaTypeHeaderValue set)
{
if (set.MatchesAllSubTypes)
{
return true;
}
if (set.Suffix.HasValue)
{
if (Suffix.HasValue)
{
return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set);
}
else
{
return false;
}
}
else
{
return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase);
}
}
private bool MatchesSubtypeWithoutSuffix(MediaTypeHeaderValue set)
{
return set.MatchesAllSubTypesWithoutSuffix ||
set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase);
}
private bool MatchesParameters(MediaTypeHeaderValue set)
{
if (set._parameters != null && set._parameters.Count != 0)
{
// Make sure all parameters in the potential superset are included locally. Fine to have additional
// parameters locally; they make this one more specific.
foreach (var parameter in set._parameters)
{
if (parameter.Name.Equals(WildcardString, StringComparison.OrdinalIgnoreCase))
{
// A parameter named "*" has no effect on media type matching, as it is only used as an indication
// that the entire media type string should be treated as a wildcard.
continue;
}
if (parameter.Name.Equals(QualityString, StringComparison.OrdinalIgnoreCase))
{
// "q" and later parameters are not involved in media type matching. Quoting the RFC: The first
// "q" parameter (if any) separates the media-range parameter(s) from the accept-params.
break;
}
var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name);
if (localParameter == null)
{
// Not found.
return false;
}
if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
return true;
}
private bool MatchesSubtypeSuffix(MediaTypeHeaderValue set)
{
// We don't have support for wildcards on suffixes alone (e.g., "application/entity+*")
// because there's no clear use case for it.
return set.Suffix.Equals(Suffix, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -0,0 +1,132 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.Net.Http.Headers
{
/// <summary>
/// Implementation of <see cref="IComparer{T}"/> that can compare accept media type header fields
/// based on their quality values (a.k.a q-values).
/// </summary>
public class MediaTypeHeaderValueComparer : IComparer<MediaTypeHeaderValue>
{
private static readonly MediaTypeHeaderValueComparer _mediaTypeComparer =
new MediaTypeHeaderValueComparer();
private MediaTypeHeaderValueComparer()
{
}
public static MediaTypeHeaderValueComparer QualityComparer
{
get { return _mediaTypeComparer; }
}
/// <inheritdoc />
/// <remarks>
/// Performs comparisons based on the arguments' quality values
/// (aka their "q-value"). Values with identical q-values are considered equal (i.e. the result is 0)
/// with the exception that suffixed subtype wildcards are considered less than subtype wildcards, subtype wildcards
/// are considered less than specific media types and full wildcards are considered less than
/// subtype wildcards. This allows callers to sort a sequence of <see cref="MediaTypeHeaderValue"/> following
/// their q-values in the order of specific media types, subtype wildcards, and last any full wildcards.
/// </remarks>
/// <example>
/// If we had a list of media types (comma separated): { text/*;q=0.8, text/*+json;q=0.8, */*;q=1, */*;q=0.8, text/plain;q=0.8 }
/// Sorting them using Compare would return: { */*;q=0.8, text/*;q=0.8, text/*+json;q=0.8, text/plain;q=0.8, */*;q=1 }
/// </example>
public int Compare(MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2)
{
if (object.ReferenceEquals(mediaType1, mediaType2))
{
return 0;
}
var returnValue = CompareBasedOnQualityFactor(mediaType1, mediaType2);
if (returnValue == 0)
{
if (!mediaType1.Type.Equals(mediaType2.Type, StringComparison.OrdinalIgnoreCase))
{
if (mediaType1.MatchesAllTypes)
{
return -1;
}
else if (mediaType2.MatchesAllTypes)
{
return 1;
}
else if (mediaType1.MatchesAllSubTypes && !mediaType2.MatchesAllSubTypes)
{
return -1;
}
else if (!mediaType1.MatchesAllSubTypes && mediaType2.MatchesAllSubTypes)
{
return 1;
}
else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return -1;
}
else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return 1;
}
}
else if (!mediaType1.SubType.Equals(mediaType2.SubType, StringComparison.OrdinalIgnoreCase))
{
if (mediaType1.MatchesAllSubTypes)
{
return -1;
}
else if (mediaType2.MatchesAllSubTypes)
{
return 1;
}
else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return -1;
}
else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return 1;
}
}
else if (!mediaType1.Suffix.Equals(mediaType2.Suffix, StringComparison.OrdinalIgnoreCase))
{
if (mediaType1.MatchesAllSubTypesWithoutSuffix)
{
return -1;
}
else if (mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return 1;
}
}
}
return returnValue;
}
private static int CompareBasedOnQualityFactor(
MediaTypeHeaderValue mediaType1,
MediaTypeHeaderValue mediaType2)
{
var mediaType1Quality = mediaType1.Quality ?? HeaderQuality.Match;
var mediaType2Quality = mediaType2.Quality ?? HeaderQuality.Match;
var qualityDifference = mediaType1Quality - mediaType2Quality;
if (qualityDifference < 0)
{
return -1;
}
else if (qualityDifference > 0)
{
return 1;
}
return 0;
}
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>HTTP header parser implementations.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>http</PackageTags>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Primitives" />
<Reference Include="System.Buffers" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,425 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
// According to the RFC, in places where a "parameter" is required, the value is mandatory
// (e.g. Media-Type, Accept). However, we don't introduce a dedicated type for it. So NameValueHeaderValue supports
// name-only values in addition to name/value pairs.
public class NameValueHeaderValue
{
private static readonly HttpHeaderParser<NameValueHeaderValue> SingleValueParser
= new GenericHeaderParser<NameValueHeaderValue>(false, GetNameValueLength);
internal static readonly HttpHeaderParser<NameValueHeaderValue> MultipleValueParser
= new GenericHeaderParser<NameValueHeaderValue>(true, GetNameValueLength);
private StringSegment _name;
private StringSegment _value;
private bool _isReadOnly;
private NameValueHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
public NameValueHeaderValue(StringSegment name)
: this(name, null)
{
}
public NameValueHeaderValue(StringSegment name, StringSegment value)
{
CheckNameValueFormat(name, value);
_name = name;
_value = value;
}
public StringSegment Name
{
get { return _name; }
}
public StringSegment Value
{
get { return _value; }
set
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
CheckValueFormat(value);
_value = value;
}
}
public bool IsReadOnly { get { return _isReadOnly; } }
/// <summary>
/// Provides a copy of this object without the cost of re-validating the values.
/// </summary>
/// <returns>A copy.</returns>
public NameValueHeaderValue Copy()
{
return new NameValueHeaderValue()
{
_name = _name,
_value = _value
};
}
public NameValueHeaderValue CopyAsReadOnly()
{
if (IsReadOnly)
{
return this;
}
return new NameValueHeaderValue()
{
_name = _name,
_value = _value,
_isReadOnly = true
};
}
public override int GetHashCode()
{
Contract.Assert(_name != null);
var nameHashCode = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name);
if (!StringSegment.IsNullOrEmpty(_value))
{
// If we have a quoted-string, then just use the hash code. If we have a token, convert to lowercase
// and retrieve the hash code.
if (_value[0] == '"')
{
return nameHashCode ^ _value.GetHashCode();
}
return nameHashCode ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value);
}
return nameHashCode;
}
public override bool Equals(object obj)
{
var other = obj as NameValueHeaderValue;
if (other == null)
{
return false;
}
if (!_name.Equals(other._name, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// RFC2616: 14.20: unquoted tokens should use case-INsensitive comparison; quoted-strings should use
// case-sensitive comparison. The RFC doesn't mention how to compare quoted-strings outside the "Expect"
// header. We treat all quoted-strings the same: case-sensitive comparison.
if (StringSegment.IsNullOrEmpty(_value))
{
return StringSegment.IsNullOrEmpty(other._value);
}
if (_value[0] == '"')
{
// We have a quoted string, so we need to do case-sensitive comparison.
return (_value.Equals(other._value, StringComparison.Ordinal));
}
else
{
return (_value.Equals(other._value, StringComparison.OrdinalIgnoreCase));
}
}
public StringSegment GetUnescapedValue()
{
if (!HeaderUtilities.IsQuoted(_value))
{
return _value;
}
return HeaderUtilities.UnescapeAsQuotedString(_value);
}
public void SetAndEscapeValue(StringSegment value)
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
if (StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length))
{
_value = value;
}
else
{
Value = HeaderUtilities.EscapeAsQuotedString(value);
}
}
public static NameValueHeaderValue Parse(StringSegment input)
{
var index = 0;
return SingleValueParser.ParseValue(input, ref index);
}
public static bool TryParse(StringSegment input, out NameValueHeaderValue parsedValue)
{
var index = 0;
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
}
public static IList<NameValueHeaderValue> ParseList(IList<string> input)
{
return MultipleValueParser.ParseValues(input);
}
public static IList<NameValueHeaderValue> ParseStrictList(IList<string> input)
{
return MultipleValueParser.ParseStrictValues(input);
}
public static bool TryParseList(IList<string> input, out IList<NameValueHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseValues(input, out parsedValues);
}
public static bool TryParseStrictList(IList<string> input, out IList<NameValueHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseStrictValues(input, out parsedValues);
}
public override string ToString()
{
if (!StringSegment.IsNullOrEmpty(_value))
{
return _name + "=" + _value;
}
return _name.ToString();
}
internal static void ToString(
IList<NameValueHeaderValue> values,
char separator,
bool leadingSeparator,
StringBuilder destination)
{
Contract.Assert(destination != null);
if ((values == null) || (values.Count == 0))
{
return;
}
for (var i = 0; i < values.Count; i++)
{
if (leadingSeparator || (destination.Length > 0))
{
destination.Append(separator);
destination.Append(' ');
}
destination.Append(values[i].Name);
if (!StringSegment.IsNullOrEmpty(values[i].Value))
{
destination.Append('=');
destination.Append(values[i].Value);
}
}
}
internal static string ToString(IList<NameValueHeaderValue> values, char separator, bool leadingSeparator)
{
if ((values == null) || (values.Count == 0))
{
return null;
}
var sb = new StringBuilder();
ToString(values, separator, leadingSeparator, sb);
return sb.ToString();
}
internal static int GetHashCode(IList<NameValueHeaderValue> values)
{
if ((values == null) || (values.Count == 0))
{
return 0;
}
var result = 0;
for (var i = 0; i < values.Count; i++)
{
result = result ^ values[i].GetHashCode();
}
return result;
}
private static int GetNameValueLength(StringSegment input, int startIndex, out NameValueHeaderValue parsedValue)
{
Contract.Requires(input != null);
Contract.Requires(startIndex >= 0);
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Parse the name, i.e. <name> in name/value string "<name>=<value>". Caller must remove
// leading whitespaces.
var nameLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (nameLength == 0)
{
return 0;
}
var name = input.Subsegment(startIndex, nameLength);
var current = startIndex + nameLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
// Parse the separator between name and value
if ((current == input.Length) || (input[current] != '='))
{
// We only have a name and that's OK. Return.
parsedValue = new NameValueHeaderValue();
parsedValue._name = name;
current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces
return current - startIndex;
}
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
// Parse the value, i.e. <value> in name/value string "<name>=<value>"
int valueLength = GetValueLength(input, current);
// Value after the '=' may be empty
// Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation.
parsedValue = new NameValueHeaderValue();
parsedValue._name = name;
parsedValue._value = input.Subsegment(current, valueLength);
current = current + valueLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces
return current - startIndex;
}
// Returns the length of a name/value list, separated by 'delimiter'. E.g. "a=b, c=d, e=f" adds 3
// name/value pairs to 'nameValueCollection' if 'delimiter' equals ','.
internal static int GetNameValueListLength(
StringSegment input,
int startIndex,
char delimiter,
IList<NameValueHeaderValue> nameValueCollection)
{
Contract.Requires(nameValueCollection != null);
Contract.Requires(startIndex >= 0);
if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length))
{
return 0;
}
var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
while (true)
{
NameValueHeaderValue parameter = null;
var nameValueLength = GetNameValueLength(input, current, out parameter);
if (nameValueLength == 0)
{
// There may be a trailing ';'
return current - startIndex;
}
nameValueCollection.Add(parameter);
current = current + nameValueLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
if ((current == input.Length) || (input[current] != delimiter))
{
// We're done and we have at least one valid name/value pair.
return current - startIndex;
}
// input[current] is 'delimiter'. Skip the delimiter and whitespaces and try to parse again.
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
}
}
public static NameValueHeaderValue Find(IList<NameValueHeaderValue> values, StringSegment name)
{
Contract.Requires((name != null) && (name.Length > 0));
if ((values == null) || (values.Count == 0))
{
return null;
}
for (var i = 0; i < values.Count; i++)
{
var value = values[i];
if (value.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
{
return value;
}
}
return null;
}
internal static int GetValueLength(StringSegment input, int startIndex)
{
Contract.Requires(input != null);
if (startIndex >= input.Length)
{
return 0;
}
var valueLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (valueLength == 0)
{
// A value can either be a token or a quoted string. Check if it is a quoted string.
if (HttpRuleParser.GetQuotedStringLength(input, startIndex, out valueLength) != HttpParseResult.Parsed)
{
// We have an invalid value. Reset the name and return.
return 0;
}
}
return valueLength;
}
private static void CheckNameValueFormat(StringSegment name, StringSegment value)
{
HeaderUtilities.CheckValidToken(name, nameof(name));
CheckValueFormat(value);
}
private static void CheckValueFormat(StringSegment value)
{
// Either value is null/empty or a valid token/quoted string
if (!(StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length)))
{
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "The header value is invalid: '{0}'", value));
}
}
private static NameValueHeaderValue CreateNameValue()
{
return new NameValueHeaderValue();
}
}
}

View File

@ -0,0 +1,91 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Microsoft.Net.Http.Headers
{
// List<T> allows 'null' values to be added. This is not what we want so we use a custom Collection<T> derived
// type to throw if 'null' gets added. Collection<T> internally uses List<T> which comes at some cost. In addition
// Collection<T>.Add() calls List<T>.InsertItem() which is an O(n) operation (compared to O(1) for List<T>.Add()).
// This type is only used for very small collections (1-2 items) to keep the impact of using Collection<T> small.
internal class ObjectCollection<T> : Collection<T>
{
internal static readonly Action<T> DefaultValidator = CheckNotNull;
internal static readonly ObjectCollection<T> EmptyReadOnlyCollection
= new ObjectCollection<T>(DefaultValidator, isReadOnly: true);
private readonly Action<T> _validator;
// We need to create a 'read-only' inner list for Collection<T> to do the right
// thing.
private static IList<T> CreateInnerList(bool isReadOnly, IEnumerable <T> other = null)
{
var list = other == null ? new List<T>() : new List<T>(other);
if (isReadOnly)
{
return new ReadOnlyCollection<T>(list);
}
else
{
return list;
}
}
public ObjectCollection()
: this(DefaultValidator)
{
}
public ObjectCollection(Action<T> validator, bool isReadOnly = false)
: base(CreateInnerList(isReadOnly))
{
_validator = validator;
}
public ObjectCollection(IEnumerable<T> other, bool isReadOnly = false)
: base(CreateInnerList(isReadOnly, other))
{
_validator = DefaultValidator;
foreach (T item in Items)
{
_validator(item);
}
}
public bool IsReadOnly => ((ICollection<T>)this).IsReadOnly;
protected override void ClearItems()
{
base.ClearItems();
}
protected override void InsertItem(int index, T item)
{
_validator(item);
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
base.RemoveItem(index);
}
protected override void SetItem(int index, T item)
{
_validator(item);
base.SetItem(index, item);
}
private static void CheckNotNull(T item)
{
// null values cannot be added to the collection.
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
}
}
}

View File

@ -0,0 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.Net.Http.Headers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,167 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.Contracts;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
public class RangeConditionHeaderValue
{
private static readonly HttpHeaderParser<RangeConditionHeaderValue> Parser
= new GenericHeaderParser<RangeConditionHeaderValue>(false, GetRangeConditionLength);
private DateTimeOffset? _lastModified;
private EntityTagHeaderValue _entityTag;
private RangeConditionHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
public RangeConditionHeaderValue(DateTimeOffset lastModified)
{
_lastModified = lastModified;
}
public RangeConditionHeaderValue(EntityTagHeaderValue entityTag)
{
if (entityTag == null)
{
throw new ArgumentNullException(nameof(entityTag));
}
_entityTag = entityTag;
}
public RangeConditionHeaderValue(string entityTag)
: this(new EntityTagHeaderValue(entityTag))
{
}
public DateTimeOffset? LastModified
{
get { return _lastModified; }
}
public EntityTagHeaderValue EntityTag
{
get { return _entityTag; }
}
public override string ToString()
{
if (_entityTag == null)
{
return HeaderUtilities.FormatDate(_lastModified.Value);
}
return _entityTag.ToString();
}
public override bool Equals(object obj)
{
var other = obj as RangeConditionHeaderValue;
if (other == null)
{
return false;
}
if (_entityTag == null)
{
return (other._lastModified != null) && (_lastModified.Value == other._lastModified.Value);
}
return _entityTag.Equals(other._entityTag);
}
public override int GetHashCode()
{
if (_entityTag == null)
{
return _lastModified.Value.GetHashCode();
}
return _entityTag.GetHashCode();
}
public static RangeConditionHeaderValue Parse(StringSegment input)
{
var index = 0;
return Parser.ParseValue(input, ref index);
}
public static bool TryParse(StringSegment input, out RangeConditionHeaderValue parsedValue)
{
var index = 0;
return Parser.TryParseValue(input, ref index, out parsedValue);
}
private static int GetRangeConditionLength(StringSegment input, int startIndex, out RangeConditionHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
parsedValue = null;
// Make sure we have at least 2 characters
if (StringSegment.IsNullOrEmpty(input) || (startIndex + 1 >= input.Length))
{
return 0;
}
var current = startIndex;
// Caller must remove leading whitespaces.
DateTimeOffset date = DateTimeOffset.MinValue;
EntityTagHeaderValue entityTag = null;
// Entity tags are quoted strings optionally preceded by "W/". By looking at the first two character we
// can determine whether the string is en entity tag or a date.
var firstChar = input[current];
var secondChar = input[current + 1];
if ((firstChar == '\"') || (((firstChar == 'w') || (firstChar == 'W')) && (secondChar == '/')))
{
// trailing whitespaces are removed by GetEntityTagLength()
var entityTagLength = EntityTagHeaderValue.GetEntityTagLength(input, current, out entityTag);
if (entityTagLength == 0)
{
return 0;
}
current = current + entityTagLength;
// RangeConditionHeaderValue only allows 1 value. There must be no delimiter/other chars after an
// entity tag.
if (current != input.Length)
{
return 0;
}
}
else
{
if (!HttpRuleParser.TryStringToDate(input.Subsegment(current), out date))
{
return 0;
}
// If we got a valid date, then the parser consumed the whole string (incl. trailing whitespaces).
current = input.Length;
}
parsedValue = new RangeConditionHeaderValue();
if (entityTag == null)
{
parsedValue._lastModified = date;
}
else
{
parsedValue._entityTag = entityTag;
}
return current - startIndex;
}
}
}

View File

@ -0,0 +1,163 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
public class RangeHeaderValue
{
private static readonly HttpHeaderParser<RangeHeaderValue> Parser
= new GenericHeaderParser<RangeHeaderValue>(false, GetRangeLength);
private StringSegment _unit;
private ICollection<RangeItemHeaderValue> _ranges;
public RangeHeaderValue()
{
_unit = HeaderUtilities.BytesUnit;
}
public RangeHeaderValue(long? from, long? to)
{
// convenience ctor: "Range: bytes=from-to"
_unit = HeaderUtilities.BytesUnit;
Ranges.Add(new RangeItemHeaderValue(from, to));
}
public StringSegment Unit
{
get { return _unit; }
set
{
HeaderUtilities.CheckValidToken(value, nameof(value));
_unit = value;
}
}
public ICollection<RangeItemHeaderValue> Ranges
{
get
{
if (_ranges == null)
{
_ranges = new ObjectCollection<RangeItemHeaderValue>();
}
return _ranges;
}
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append(_unit);
sb.Append('=');
var first = true;
foreach (var range in Ranges)
{
if (first)
{
first = false;
}
else
{
sb.Append(", ");
}
sb.Append(range.From);
sb.Append('-');
sb.Append(range.To);
}
return sb.ToString();
}
public override bool Equals(object obj)
{
var other = obj as RangeHeaderValue;
if (other == null)
{
return false;
}
return StringSegment.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase) &&
HeaderUtilities.AreEqualCollections(Ranges, other.Ranges);
}
public override int GetHashCode()
{
var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_unit);
foreach (var range in Ranges)
{
result = result ^ range.GetHashCode();
}
return result;
}
public static RangeHeaderValue Parse(StringSegment input)
{
var index = 0;
return Parser.ParseValue(input, ref index);
}
public static bool TryParse(StringSegment input, out RangeHeaderValue parsedValue)
{
var index = 0;
return Parser.TryParseValue(input, ref index, out parsedValue);
}
private static int GetRangeLength(StringSegment input, int startIndex, out RangeHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Parse the unit string: <unit> in '<unit>=<from1>-<to1>, <from2>-<to2>'
var unitLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (unitLength == 0)
{
return 0;
}
RangeHeaderValue result = new RangeHeaderValue();
result._unit = input.Subsegment(startIndex, unitLength);
var current = startIndex + unitLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
if ((current == input.Length) || (input[current] != '='))
{
return 0;
}
current++; // skip '=' separator
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
var rangesLength = RangeItemHeaderValue.GetRangeItemListLength(input, current, result.Ranges);
if (rangesLength == 0)
{
return 0;
}
current = current + rangesLength;
Contract.Assert(current == input.Length, "GetRangeItemListLength() should consume the whole string or fail.");
parsedValue = result;
return current - startIndex;
}
}
}

View File

@ -0,0 +1,229 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
public class RangeItemHeaderValue
{
private long? _from;
private long? _to;
public RangeItemHeaderValue(long? from, long? to)
{
if (!from.HasValue && !to.HasValue)
{
throw new ArgumentException("Invalid header range.");
}
if (from.HasValue && (from.Value < 0))
{
throw new ArgumentOutOfRangeException(nameof(from));
}
if (to.HasValue && (to.Value < 0))
{
throw new ArgumentOutOfRangeException(nameof(to));
}
if (from.HasValue && to.HasValue && (from.Value > to.Value))
{
throw new ArgumentOutOfRangeException(nameof(from));
}
_from = from;
_to = to;
}
public long? From
{
get { return _from; }
}
public long? To
{
get { return _to; }
}
public override string ToString()
{
if (!_from.HasValue)
{
return "-" + _to.Value.ToString(NumberFormatInfo.InvariantInfo);
}
else if (!_to.HasValue)
{
return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-";
}
return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-" +
_to.Value.ToString(NumberFormatInfo.InvariantInfo);
}
public override bool Equals(object obj)
{
var other = obj as RangeItemHeaderValue;
if (other == null)
{
return false;
}
return ((_from == other._from) && (_to == other._to));
}
public override int GetHashCode()
{
if (!_from.HasValue)
{
return _to.GetHashCode();
}
else if (!_to.HasValue)
{
return _from.GetHashCode();
}
return _from.GetHashCode() ^ _to.GetHashCode();
}
// Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty
// list segments are allowed, e.g. ",1-2, , 3-4,,".
internal static int GetRangeItemListLength(
StringSegment input,
int startIndex,
ICollection<RangeItemHeaderValue> rangeCollection)
{
Contract.Requires(rangeCollection != null);
Contract.Requires(startIndex >= 0);
Contract.Ensures((Contract.Result<int>() == 0) || (rangeCollection.Count > 0),
"If we can parse the string, then we expect to have at least one range item.");
if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length))
{
return 0;
}
// Empty segments are allowed, so skip all delimiter-only segments (e.g. ", ,").
var separatorFound = false;
var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, startIndex, true, out separatorFound);
// It's OK if we didn't find leading separator characters. Ignore 'separatorFound'.
if (current == input.Length)
{
return 0;
}
RangeItemHeaderValue range = null;
while (true)
{
var rangeLength = GetRangeItemLength(input, current, out range);
if (rangeLength == 0)
{
return 0;
}
rangeCollection.Add(range);
current = current + rangeLength;
current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, current, true, out separatorFound);
// If the string is not consumed, we must have a delimiter, otherwise the string is not a valid
// range list.
if ((current < input.Length) && !separatorFound)
{
return 0;
}
if (current == input.Length)
{
return current - startIndex;
}
}
}
internal static int GetRangeItemLength(StringSegment input, int startIndex, out RangeItemHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
// This parser parses number ranges: e.g. '1-2', '1-', '-2'.
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Caller must remove leading whitespaces. If not, we'll return 0.
var current = startIndex;
// Try parse the first value of a value pair.
var fromStartIndex = current;
var fromLength = HttpRuleParser.GetNumberLength(input, current, false);
if (fromLength > HttpRuleParser.MaxInt64Digits)
{
return 0;
}
current = current + fromLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
// Afer the first value, the '-' character must follow.
if ((current == input.Length) || (input[current] != '-'))
{
// We need a '-' character otherwise this can't be a valid range.
return 0;
}
current++; // skip the '-' character
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
var toStartIndex = current;
var toLength = 0;
// If we didn't reach the end of the string, try parse the second value of the range.
if (current < input.Length)
{
toLength = HttpRuleParser.GetNumberLength(input, current, false);
if (toLength > HttpRuleParser.MaxInt64Digits)
{
return 0;
}
current = current + toLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
}
if ((fromLength == 0) && (toLength == 0))
{
return 0; // At least one value must be provided in order to be a valid range.
}
// Try convert first value to int64
long from = 0;
if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from))
{
return 0;
}
// Try convert second value to int64
long to = 0;
if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to))
{
return 0;
}
// 'from' must not be greater than 'to'
if ((fromLength > 0) && (toLength > 0) && (from > to))
{
return 0;
}
parsedValue = new RangeItemHeaderValue((fromLength == 0 ? (long?)null : (long?)from),
(toLength == 0 ? (long?)null : (long?)to));
return current - startIndex;
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.Net.Http.Headers
{
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
public enum SameSiteMode
{
None = 0,
Lax,
Strict
}
}

View File

@ -0,0 +1,523 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
// http://tools.ietf.org/html/rfc6265
public class SetCookieHeaderValue
{
private const string ExpiresToken = "expires";
private const string MaxAgeToken = "max-age";
private const string DomainToken = "domain";
private const string PathToken = "path";
private const string SecureToken = "secure";
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
private const string SameSiteToken = "samesite";
private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower();
private const string HttpOnlyToken = "httponly";
private const string SeparatorToken = "; ";
private const string EqualsToken = "=";
private const string DefaultPath = "/"; // TODO: Used?
private static readonly HttpHeaderParser<SetCookieHeaderValue> SingleValueParser
= new GenericHeaderParser<SetCookieHeaderValue>(false, GetSetCookieLength);
private static readonly HttpHeaderParser<SetCookieHeaderValue> MultipleValueParser
= new GenericHeaderParser<SetCookieHeaderValue>(true, GetSetCookieLength);
private StringSegment _name;
private StringSegment _value;
private SetCookieHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
public SetCookieHeaderValue(StringSegment name)
: this(name, StringSegment.Empty)
{
}
public SetCookieHeaderValue(StringSegment name, StringSegment value)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
Name = name;
Value = value;
}
public StringSegment Name
{
get { return _name; }
set
{
CookieHeaderValue.CheckNameFormat(value, nameof(value));
_name = value;
}
}
public StringSegment Value
{
get { return _value; }
set
{
CookieHeaderValue.CheckValueFormat(value, nameof(value));
_value = value;
}
}
public DateTimeOffset? Expires { get; set; }
public TimeSpan? MaxAge { get; set; }
public StringSegment Domain { get; set; }
public StringSegment Path { get; set; }
public bool Secure { get; set; }
public SameSiteMode SameSite { get; set; }
public bool HttpOnly { get; set; }
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
public override string ToString()
{
var length = _name.Length + EqualsToken.Length + _value.Length;
string expires = null;
string maxAge = null;
string sameSite = null;
if (Expires.HasValue)
{
expires = HeaderUtilities.FormatDate(Expires.Value);
length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + expires.Length;
}
if (MaxAge.HasValue)
{
maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds);
length += SeparatorToken.Length + MaxAgeToken.Length + EqualsToken.Length + maxAge.Length;
}
if (Domain != null)
{
length += SeparatorToken.Length + DomainToken.Length + EqualsToken.Length + Domain.Length;
}
if (Path != null)
{
length += SeparatorToken.Length + PathToken.Length + EqualsToken.Length + Path.Length;
}
if (Secure)
{
length += SeparatorToken.Length + SecureToken.Length;
}
if (SameSite != SameSiteMode.None)
{
sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
}
if (HttpOnly)
{
length += SeparatorToken.Length + HttpOnlyToken.Length;
}
var sb = new InplaceStringBuilder(length);
sb.Append(_name);
sb.Append(EqualsToken);
sb.Append(_value);
if (expires != null)
{
AppendSegment(ref sb, ExpiresToken, expires);
}
if (maxAge != null)
{
AppendSegment(ref sb, MaxAgeToken, maxAge);
}
if (Domain != null)
{
AppendSegment(ref sb, DomainToken, Domain);
}
if (Path != null)
{
AppendSegment(ref sb, PathToken, Path);
}
if (Secure)
{
AppendSegment(ref sb, SecureToken, null);
}
if (SameSite != SameSiteMode.None)
{
AppendSegment(ref sb, SameSiteToken, sameSite);
}
if (HttpOnly)
{
AppendSegment(ref sb, HttpOnlyToken, null);
}
return sb.ToString();
}
private static void AppendSegment(ref InplaceStringBuilder builder, StringSegment name, StringSegment value)
{
builder.Append(SeparatorToken);
builder.Append(name);
if (value != null)
{
builder.Append(EqualsToken);
builder.Append(value);
}
}
/// <summary>
/// Append string representation of this <see cref="SetCookieHeaderValue"/> to given
/// <paramref name="builder"/>.
/// </summary>
/// <param name="builder">
/// The <see cref="StringBuilder"/> to receive the string representation of this
/// <see cref="SetCookieHeaderValue"/>.
/// </param>
public void AppendToStringBuilder(StringBuilder builder)
{
builder.Append(_name);
builder.Append("=");
builder.Append(_value);
if (Expires.HasValue)
{
AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value));
}
if (MaxAge.HasValue)
{
AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds));
}
if (Domain != null)
{
AppendSegment(builder, DomainToken, Domain);
}
if (Path != null)
{
AppendSegment(builder, PathToken, Path);
}
if (Secure)
{
AppendSegment(builder, SecureToken, null);
}
if (SameSite != SameSiteMode.None)
{
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
}
if (HttpOnly)
{
AppendSegment(builder, HttpOnlyToken, null);
}
}
private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value)
{
builder.Append("; ");
builder.Append(name);
if (value != null)
{
builder.Append("=");
builder.Append(value);
}
}
public static SetCookieHeaderValue Parse(StringSegment input)
{
var index = 0;
return SingleValueParser.ParseValue(input, ref index);
}
public static bool TryParse(StringSegment input, out SetCookieHeaderValue parsedValue)
{
var index = 0;
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
}
public static IList<SetCookieHeaderValue> ParseList(IList<string> inputs)
{
return MultipleValueParser.ParseValues(inputs);
}
public static IList<SetCookieHeaderValue> ParseStrictList(IList<string> inputs)
{
return MultipleValueParser.ParseStrictValues(inputs);
}
public static bool TryParseList(IList<string> inputs, out IList<SetCookieHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
}
public static bool TryParseStrictList(IList<string> inputs, out IList<SetCookieHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
}
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
var offset = startIndex;
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length))
{
return 0;
}
var result = new SetCookieHeaderValue();
// The caller should have already consumed any leading whitespace, commas, etc..
// Name=value;
// Name
var itemLength = HttpRuleParser.GetTokenLength(input, offset);
if (itemLength == 0)
{
return 0;
}
result._name = input.Subsegment(offset, itemLength);
offset += itemLength;
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
// value or "quoted value"
// The value may be empty
result._value = CookieHeaderValue.GetCookieValue(input, ref offset);
// *(';' SP cookie-av)
while (offset < input.Length)
{
if (input[offset] == ',')
{
// Divider between headers
break;
}
if (input[offset] != ';')
{
// Expecting a ';' between parameters
return 0;
}
offset++;
offset += HttpRuleParser.GetWhitespaceLength(input, offset);
// cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / samesite-av / httponly-av / extension-av
itemLength = HttpRuleParser.GetTokenLength(input, offset);
if (itemLength == 0)
{
// Trailing ';' or leading into garbage. Let the next parser fail.
break;
}
var token = input.Subsegment(offset, itemLength);
offset += itemLength;
// expires-av = "Expires=" sane-cookie-date
if (StringSegment.Equals(token, ExpiresToken, StringComparison.OrdinalIgnoreCase))
{
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
var dateString = ReadToSemicolonOrEnd(input, ref offset);
DateTimeOffset expirationDate;
if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate))
{
// Invalid expiration date, abort
return 0;
}
result.Expires = expirationDate;
}
// max-age-av = "Max-Age=" non-zero-digit *DIGIT
else if (StringSegment.Equals(token, MaxAgeToken, StringComparison.OrdinalIgnoreCase))
{
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
itemLength = HttpRuleParser.GetNumberLength(input, offset, allowDecimal: false);
if (itemLength == 0)
{
return 0;
}
var numberString = input.Subsegment(offset, itemLength);
long maxAge;
if (!HeaderUtilities.TryParseNonNegativeInt64(numberString, out maxAge))
{
// Invalid expiration date, abort
return 0;
}
result.MaxAge = TimeSpan.FromSeconds(maxAge);
offset += itemLength;
}
// domain-av = "Domain=" domain-value
// domain-value = <subdomain> ; defined in [RFC1034], Section 3.5, as enhanced by [RFC1123], Section 2.1
else if (StringSegment.Equals(token, DomainToken, StringComparison.OrdinalIgnoreCase))
{
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
// We don't do any detailed validation on the domain.
result.Domain = ReadToSemicolonOrEnd(input, ref offset);
}
// path-av = "Path=" path-value
// path-value = <any CHAR except CTLs or ";">
else if (StringSegment.Equals(token, PathToken, StringComparison.OrdinalIgnoreCase))
{
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
// We don't do any detailed validation on the path.
result.Path = ReadToSemicolonOrEnd(input, ref offset);
}
// secure-av = "Secure"
else if (StringSegment.Equals(token, SecureToken, StringComparison.OrdinalIgnoreCase))
{
result.Secure = true;
}
// samesite-av = "SameSite" / "SameSite=" samesite-value
// samesite-value = "Strict" / "Lax"
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
{
if (!ReadEqualsSign(input, ref offset))
{
result.SameSite = SameSiteMode.Strict;
}
else
{
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
{
result.SameSite = SameSiteMode.Lax;
}
else
{
result.SameSite = SameSiteMode.Strict;
}
}
}
// httponly-av = "HttpOnly"
else if (StringSegment.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase))
{
result.HttpOnly = true;
}
// extension-av = <any CHAR except CTLs or ";">
else
{
// TODO: skip it? Store it in a list?
}
}
parsedValue = result;
return offset - startIndex;
}
private static bool ReadEqualsSign(StringSegment input, ref int offset)
{
// = (no spaces)
if (offset >= input.Length || input[offset] != '=')
{
return false;
}
offset++;
return true;
}
private static StringSegment ReadToSemicolonOrEnd(StringSegment input, ref int offset)
{
var end = input.IndexOf(';', offset);
if (end < 0)
{
// Remainder of the string
end = input.Length;
}
var itemLength = end - offset;
var result = input.Subsegment(offset, itemLength);
offset += itemLength;
return result;
}
public override bool Equals(object obj)
{
var other = obj as SetCookieHeaderValue;
if (other == null)
{
return false;
}
return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase)
&& StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase)
&& Expires.Equals(other.Expires)
&& MaxAge.Equals(other.MaxAge)
&& StringSegment.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase)
&& StringSegment.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)
&& Secure == other.Secure
&& SameSite == other.SameSite
&& HttpOnly == other.HttpOnly;
}
public override int GetHashCode()
{
return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name)
^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value)
^ (Expires.HasValue ? Expires.GetHashCode() : 0)
^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0)
^ (Domain != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0)
^ (Path != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0)
^ Secure.GetHashCode()
^ SameSite.GetHashCode()
^ HttpOnly.GetHashCode();
}
}
}

View File

@ -0,0 +1,222 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
public class StringWithQualityHeaderValue
{
private static readonly HttpHeaderParser<StringWithQualityHeaderValue> SingleValueParser
= new GenericHeaderParser<StringWithQualityHeaderValue>(false, GetStringWithQualityLength);
private static readonly HttpHeaderParser<StringWithQualityHeaderValue> MultipleValueParser
= new GenericHeaderParser<StringWithQualityHeaderValue>(true, GetStringWithQualityLength);
private StringSegment _value;
private double? _quality;
private StringWithQualityHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
public StringWithQualityHeaderValue(StringSegment value)
{
HeaderUtilities.CheckValidToken(value, nameof(value));
_value = value;
}
public StringWithQualityHeaderValue(StringSegment value, double quality)
{
HeaderUtilities.CheckValidToken(value, nameof(value));
if ((quality < 0) || (quality > 1))
{
throw new ArgumentOutOfRangeException(nameof(quality));
}
_value = value;
_quality = quality;
}
public StringSegment Value
{
get { return _value; }
}
public double? Quality
{
get { return _quality; }
}
public override string ToString()
{
if (_quality.HasValue)
{
return _value + "; q=" + _quality.Value.ToString("0.0##", NumberFormatInfo.InvariantInfo);
}
return _value.ToString();
}
public override bool Equals(object obj)
{
var other = obj as StringWithQualityHeaderValue;
if (other == null)
{
return false;
}
if (!StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (_quality.HasValue)
{
// Note that we don't consider double.Epsilon here. We really consider two values equal if they're
// actually equal. This makes sure that we also get the same hashcode for two values considered equal
// by Equals().
return other._quality.HasValue && (_quality.Value == other._quality.Value);
}
// If we don't have a quality value, then 'other' must also have no quality assigned in order to be
// considered equal.
return !other._quality.HasValue;
}
public override int GetHashCode()
{
var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value);
if (_quality.HasValue)
{
result = result ^ _quality.Value.GetHashCode();
}
return result;
}
public static StringWithQualityHeaderValue Parse(StringSegment input)
{
var index = 0;
return SingleValueParser.ParseValue(input, ref index);
}
public static bool TryParse(StringSegment input, out StringWithQualityHeaderValue parsedValue)
{
var index = 0;
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
}
public static IList<StringWithQualityHeaderValue> ParseList(IList<string> input)
{
return MultipleValueParser.ParseValues(input);
}
public static IList<StringWithQualityHeaderValue> ParseStrictList(IList<string> input)
{
return MultipleValueParser.ParseStrictValues(input);
}
public static bool TryParseList(IList<string> input, out IList<StringWithQualityHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseValues(input, out parsedValues);
}
public static bool TryParseStrictList(IList<string> input, out IList<StringWithQualityHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseStrictValues(input, out parsedValues);
}
private static int GetStringWithQualityLength(StringSegment input, int startIndex, out StringWithQualityHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
parsedValue = null;
if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Parse the value string: <value> in '<value>; q=<quality>'
var valueLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (valueLength == 0)
{
return 0;
}
StringWithQualityHeaderValue result = new StringWithQualityHeaderValue();
result._value = input.Subsegment(startIndex, valueLength);
var current = startIndex + valueLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
if ((current == input.Length) || (input[current] != ';'))
{
parsedValue = result;
return current - startIndex; // we have a valid token, but no quality.
}
current++; // skip ';' separator
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
// If we found a ';' separator, it must be followed by a quality information
if (!TryReadQuality(input, result, ref current))
{
return 0;
}
parsedValue = result;
return current - startIndex;
}
private static bool TryReadQuality(StringSegment input, StringWithQualityHeaderValue result, ref int index)
{
var current = index;
// See if we have a quality value by looking for "q"
if ((current == input.Length) || ((input[current] != 'q') && (input[current] != 'Q')))
{
return false;
}
current++; // skip 'q' identifier
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
// If we found "q" it must be followed by "="
if ((current == input.Length) || (input[current] != '='))
{
return false;
}
current++; // skip '=' separator
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
if (current == input.Length)
{
return false;
}
if (!HeaderUtilities.TryParseQualityDouble(input, current, out var quality, out var qualityLength))
{
return false;
}
result._quality = quality;
current = current + qualityLength;
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
index = current;
return true;
}
}
}

View File

@ -0,0 +1,83 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
/// <summary>
/// Implementation of <see cref="IComparer{T}"/> that can compare content negotiation header fields
/// based on their quality values (a.k.a q-values). This applies to values used in accept-charset,
/// accept-encoding, accept-language and related header fields with similar syntax rules. See
/// <see cref="MediaTypeHeaderValueComparer"/> for a comparer for media type
/// q-values.
/// </summary>
public class StringWithQualityHeaderValueComparer : IComparer<StringWithQualityHeaderValue>
{
private static readonly StringWithQualityHeaderValueComparer _qualityComparer =
new StringWithQualityHeaderValueComparer();
private StringWithQualityHeaderValueComparer()
{
}
public static StringWithQualityHeaderValueComparer QualityComparer
{
get { return _qualityComparer; }
}
/// <summary>
/// Compares two <see cref="StringWithQualityHeaderValue"/> based on their quality value
/// (a.k.a their "q-value").
/// Values with identical q-values are considered equal (i.e the result is 0) with the exception of wild-card
/// values (i.e. a value of "*") which are considered less than non-wild-card values. This allows to sort
/// a sequence of <see cref="StringWithQualityHeaderValue"/> following their q-values ending up with any
/// wild-cards at the end.
/// </summary>
/// <param name="stringWithQuality1">The first value to compare.</param>
/// <param name="stringWithQuality2">The second value to compare</param>
/// <returns>The result of the comparison.</returns>
public int Compare(
StringWithQualityHeaderValue stringWithQuality1,
StringWithQualityHeaderValue stringWithQuality2)
{
if (stringWithQuality1 == null)
{
throw new ArgumentNullException(nameof(stringWithQuality1));
}
if (stringWithQuality2 == null)
{
throw new ArgumentNullException(nameof(stringWithQuality2));
}
var quality1 = stringWithQuality1.Quality ?? HeaderQuality.Match;
var quality2 = stringWithQuality2.Quality ?? HeaderQuality.Match;
var qualityDifference = quality1 - quality2;
if (qualityDifference < 0)
{
return -1;
}
else if (qualityDifference > 0)
{
return 1;
}
if (!StringSegment.Equals(stringWithQuality1.Value, stringWithQuality2.Value, StringComparison.OrdinalIgnoreCase))
{
if (StringSegment.Equals(stringWithQuality1.Value, "*", StringComparison.Ordinal))
{
return -1;
}
else if (StringSegment.Equals(stringWithQuality2.Value, "*", StringComparison.Ordinal))
{
return 1;
}
}
return 0;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,599 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class CacheControlHeaderValueTest
{
[Fact]
public void Properties_SetAndGetAllProperties_SetValueReturnedInGetter()
{
var cacheControl = new CacheControlHeaderValue();
// Bool properties
cacheControl.NoCache = true;
Assert.True(cacheControl.NoCache, "NoCache");
cacheControl.NoStore = true;
Assert.True(cacheControl.NoStore, "NoStore");
cacheControl.MaxStale = true;
Assert.True(cacheControl.MaxStale, "MaxStale");
cacheControl.NoTransform = true;
Assert.True(cacheControl.NoTransform, "NoTransform");
cacheControl.OnlyIfCached = true;
Assert.True(cacheControl.OnlyIfCached, "OnlyIfCached");
cacheControl.Public = true;
Assert.True(cacheControl.Public, "Public");
cacheControl.Private = true;
Assert.True(cacheControl.Private, "Private");
cacheControl.MustRevalidate = true;
Assert.True(cacheControl.MustRevalidate, "MustRevalidate");
cacheControl.ProxyRevalidate = true;
Assert.True(cacheControl.ProxyRevalidate, "ProxyRevalidate");
// TimeSpan properties
TimeSpan timeSpan = new TimeSpan(1, 2, 3);
cacheControl.MaxAge = timeSpan;
Assert.Equal(timeSpan, cacheControl.MaxAge);
cacheControl.SharedMaxAge = timeSpan;
Assert.Equal(timeSpan, cacheControl.SharedMaxAge);
cacheControl.MaxStaleLimit = timeSpan;
Assert.Equal(timeSpan, cacheControl.MaxStaleLimit);
cacheControl.MinFresh = timeSpan;
Assert.Equal(timeSpan, cacheControl.MinFresh);
// String collection properties
Assert.NotNull(cacheControl.NoCacheHeaders);
Assert.Throws<ArgumentException>(() => cacheControl.NoCacheHeaders.Add(null));
Assert.Throws<FormatException>(() => cacheControl.NoCacheHeaders.Add("invalid token"));
cacheControl.NoCacheHeaders.Add("token");
Assert.Equal(1, cacheControl.NoCacheHeaders.Count);
Assert.Equal("token", cacheControl.NoCacheHeaders.First());
Assert.NotNull(cacheControl.PrivateHeaders);
Assert.Throws<ArgumentException>(() => cacheControl.PrivateHeaders.Add(null));
Assert.Throws<FormatException>(() => cacheControl.PrivateHeaders.Add("invalid token"));
cacheControl.PrivateHeaders.Add("token");
Assert.Equal(1, cacheControl.PrivateHeaders.Count);
Assert.Equal("token", cacheControl.PrivateHeaders.First());
// NameValueHeaderValue collection property
Assert.NotNull(cacheControl.Extensions);
Assert.Throws<ArgumentNullException>(() => cacheControl.Extensions.Add(null));
cacheControl.Extensions.Add(new NameValueHeaderValue("name", "value"));
Assert.Equal(1, cacheControl.Extensions.Count);
Assert.Equal(new NameValueHeaderValue("name", "value"), cacheControl.Extensions.First());
}
[Fact]
public void ToString_UseRequestDirectiveValues_AllSerializedCorrectly()
{
var cacheControl = new CacheControlHeaderValue();
Assert.Equal("", cacheControl.ToString());
// Note that we allow all combinations of all properties even though the RFC specifies rules what value
// can be used together.
// Also for property pairs (bool property + collection property) like 'NoCache' and 'NoCacheHeaders' the
// caller needs to set the bool property in order for the collection to be populated as string.
// Cache Request Directive sample
cacheControl.NoStore = true;
Assert.Equal("no-store", cacheControl.ToString());
cacheControl.NoCache = true;
Assert.Equal("no-store, no-cache", cacheControl.ToString());
cacheControl.MaxAge = new TimeSpan(0, 1, 10);
Assert.Equal("no-store, no-cache, max-age=70", cacheControl.ToString());
cacheControl.MaxStale = true;
Assert.Equal("no-store, no-cache, max-age=70, max-stale", cacheControl.ToString());
cacheControl.MaxStaleLimit = new TimeSpan(0, 2, 5);
Assert.Equal("no-store, no-cache, max-age=70, max-stale=125", cacheControl.ToString());
cacheControl.MinFresh = new TimeSpan(0, 3, 0);
Assert.Equal("no-store, no-cache, max-age=70, max-stale=125, min-fresh=180", cacheControl.ToString());
cacheControl = new CacheControlHeaderValue();
cacheControl.NoTransform = true;
Assert.Equal("no-transform", cacheControl.ToString());
cacheControl.OnlyIfCached = true;
Assert.Equal("no-transform, only-if-cached", cacheControl.ToString());
cacheControl.Extensions.Add(new NameValueHeaderValue("custom"));
cacheControl.Extensions.Add(new NameValueHeaderValue("customName", "customValue"));
Assert.Equal("no-transform, only-if-cached, custom, customName=customValue", cacheControl.ToString());
cacheControl = new CacheControlHeaderValue();
cacheControl.Extensions.Add(new NameValueHeaderValue("custom"));
Assert.Equal("custom", cacheControl.ToString());
}
[Fact]
public void ToString_UseResponseDirectiveValues_AllSerializedCorrectly()
{
var cacheControl = new CacheControlHeaderValue();
Assert.Equal("", cacheControl.ToString());
cacheControl.NoCache = true;
Assert.Equal("no-cache", cacheControl.ToString());
cacheControl.NoCacheHeaders.Add("token1");
Assert.Equal("no-cache=\"token1\"", cacheControl.ToString());
cacheControl.Public = true;
Assert.Equal("public, no-cache=\"token1\"", cacheControl.ToString());
cacheControl = new CacheControlHeaderValue();
cacheControl.Private = true;
Assert.Equal("private", cacheControl.ToString());
cacheControl.PrivateHeaders.Add("token2");
cacheControl.PrivateHeaders.Add("token3");
Assert.Equal("private=\"token2, token3\"", cacheControl.ToString());
cacheControl.MustRevalidate = true;
Assert.Equal("must-revalidate, private=\"token2, token3\"", cacheControl.ToString());
cacheControl.ProxyRevalidate = true;
Assert.Equal("must-revalidate, proxy-revalidate, private=\"token2, token3\"", cacheControl.ToString());
}
[Fact]
public void GetHashCode_CompareValuesWithBoolFieldsSet_MatchExpectation()
{
// Verify that different bool fields return different hash values.
var values = new CacheControlHeaderValue[9];
for (int i = 0; i < values.Length; i++)
{
values[i] = new CacheControlHeaderValue();
}
values[0].ProxyRevalidate = true;
values[1].NoCache = true;
values[2].NoStore = true;
values[3].MaxStale = true;
values[4].NoTransform = true;
values[5].OnlyIfCached = true;
values[6].Public = true;
values[7].Private = true;
values[8].MustRevalidate = true;
// Only one bool field set. All hash codes should differ
for (int i = 0; i < values.Length; i++)
{
for (int j = 0; j < values.Length; j++)
{
if (i != j)
{
CompareHashCodes(values[i], values[j], false);
}
}
}
// Validate that two instances with the same bool fields set are equal.
values[0].NoCache = true;
CompareHashCodes(values[0], values[1], false);
values[1].ProxyRevalidate = true;
CompareHashCodes(values[0], values[1], true);
}
[Fact]
public void GetHashCode_CompareValuesWithTimeSpanFieldsSet_MatchExpectation()
{
// Verify that different timespan fields return different hash values.
var values = new CacheControlHeaderValue[4];
for (int i = 0; i < values.Length; i++)
{
values[i] = new CacheControlHeaderValue();
}
values[0].MaxAge = new TimeSpan(0, 1, 1);
values[1].MaxStaleLimit = new TimeSpan(0, 1, 1);
values[2].MinFresh = new TimeSpan(0, 1, 1);
values[3].SharedMaxAge = new TimeSpan(0, 1, 1);
// Only one timespan field set. All hash codes should differ
for (int i = 0; i < values.Length; i++)
{
for (int j = 0; j < values.Length; j++)
{
if (i != j)
{
CompareHashCodes(values[i], values[j], false);
}
}
}
values[0].MaxStaleLimit = new TimeSpan(0, 1, 2);
CompareHashCodes(values[0], values[1], false);
values[1].MaxAge = new TimeSpan(0, 1, 1);
values[1].MaxStaleLimit = new TimeSpan(0, 1, 2);
CompareHashCodes(values[0], values[1], true);
}
[Fact]
public void GetHashCode_CompareCollectionFieldsSet_MatchExpectation()
{
var cacheControl1 = new CacheControlHeaderValue();
var cacheControl2 = new CacheControlHeaderValue();
var cacheControl3 = new CacheControlHeaderValue();
var cacheControl4 = new CacheControlHeaderValue();
var cacheControl5 = new CacheControlHeaderValue();
cacheControl1.NoCache = true;
cacheControl1.NoCacheHeaders.Add("token2");
cacheControl2.NoCache = true;
cacheControl2.NoCacheHeaders.Add("token1");
cacheControl2.NoCacheHeaders.Add("token2");
CompareHashCodes(cacheControl1, cacheControl2, false);
cacheControl1.NoCacheHeaders.Add("token1");
CompareHashCodes(cacheControl1, cacheControl2, true);
// Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders
// have the same values, the hash code will be different.
cacheControl3.Private = true;
cacheControl3.PrivateHeaders.Add("token2");
CompareHashCodes(cacheControl1, cacheControl3, false);
cacheControl4.Extensions.Add(new NameValueHeaderValue("custom"));
CompareHashCodes(cacheControl1, cacheControl4, false);
cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
cacheControl5.Extensions.Add(new NameValueHeaderValue("custom"));
CompareHashCodes(cacheControl4, cacheControl5, false);
cacheControl4.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
CompareHashCodes(cacheControl4, cacheControl5, true);
}
[Fact]
public void Equals_CompareValuesWithBoolFieldsSet_MatchExpectation()
{
// Verify that different bool fields return different hash values.
var values = new CacheControlHeaderValue[9];
for (int i = 0; i < values.Length; i++)
{
values[i] = new CacheControlHeaderValue();
}
values[0].ProxyRevalidate = true;
values[1].NoCache = true;
values[2].NoStore = true;
values[3].MaxStale = true;
values[4].NoTransform = true;
values[5].OnlyIfCached = true;
values[6].Public = true;
values[7].Private = true;
values[8].MustRevalidate = true;
// Only one bool field set. All hash codes should differ
for (int i = 0; i < values.Length; i++)
{
for (int j = 0; j < values.Length; j++)
{
if (i != j)
{
CompareValues(values[i], values[j], false);
}
}
}
// Validate that two instances with the same bool fields set are equal.
values[0].NoCache = true;
CompareValues(values[0], values[1], false);
values[1].ProxyRevalidate = true;
CompareValues(values[0], values[1], true);
}
[Fact]
public void Equals_CompareValuesWithTimeSpanFieldsSet_MatchExpectation()
{
// Verify that different timespan fields return different hash values.
var values = new CacheControlHeaderValue[4];
for (int i = 0; i < values.Length; i++)
{
values[i] = new CacheControlHeaderValue();
}
values[0].MaxAge = new TimeSpan(0, 1, 1);
values[1].MaxStaleLimit = new TimeSpan(0, 1, 1);
values[2].MinFresh = new TimeSpan(0, 1, 1);
values[3].SharedMaxAge = new TimeSpan(0, 1, 1);
// Only one timespan field set. All hash codes should differ
for (int i = 0; i < values.Length; i++)
{
for (int j = 0; j < values.Length; j++)
{
if (i != j)
{
CompareValues(values[i], values[j], false);
}
}
}
values[0].MaxStaleLimit = new TimeSpan(0, 1, 2);
CompareValues(values[0], values[1], false);
values[1].MaxAge = new TimeSpan(0, 1, 1);
values[1].MaxStaleLimit = new TimeSpan(0, 1, 2);
CompareValues(values[0], values[1], true);
var value1 = new CacheControlHeaderValue();
value1.MaxStale = true;
var value2 = new CacheControlHeaderValue();
value2.MaxStale = true;
CompareValues(value1, value2, true);
value2.MaxStaleLimit = new TimeSpan(1, 2, 3);
CompareValues(value1, value2, false);
}
[Fact]
public void Equals_CompareCollectionFieldsSet_MatchExpectation()
{
var cacheControl1 = new CacheControlHeaderValue();
var cacheControl2 = new CacheControlHeaderValue();
var cacheControl3 = new CacheControlHeaderValue();
var cacheControl4 = new CacheControlHeaderValue();
var cacheControl5 = new CacheControlHeaderValue();
var cacheControl6 = new CacheControlHeaderValue();
cacheControl1.NoCache = true;
cacheControl1.NoCacheHeaders.Add("token2");
Assert.False(cacheControl1.Equals(null), "Compare with 'null'");
cacheControl2.NoCache = true;
cacheControl2.NoCacheHeaders.Add("token1");
cacheControl2.NoCacheHeaders.Add("token2");
CompareValues(cacheControl1, cacheControl2, false);
cacheControl1.NoCacheHeaders.Add("token1");
CompareValues(cacheControl1, cacheControl2, true);
// Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders
// have the same values, the hash code will be different.
cacheControl3.Private = true;
cacheControl3.PrivateHeaders.Add("token2");
CompareValues(cacheControl1, cacheControl3, false);
cacheControl4.Private = true;
cacheControl4.PrivateHeaders.Add("token3");
CompareValues(cacheControl3, cacheControl4, false);
cacheControl5.Extensions.Add(new NameValueHeaderValue("custom"));
CompareValues(cacheControl1, cacheControl5, false);
cacheControl6.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
cacheControl6.Extensions.Add(new NameValueHeaderValue("custom"));
CompareValues(cacheControl5, cacheControl6, false);
cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
CompareValues(cacheControl5, cacheControl6, true);
}
[Fact]
public void TryParse_DifferentValidScenarios_AllReturnTrue()
{
var expected = new CacheControlHeaderValue();
expected.NoCache = true;
CheckValidTryParse(" , no-cache ,,", expected);
expected = new CacheControlHeaderValue();
expected.NoCache = true;
expected.NoCacheHeaders.Add("token1");
expected.NoCacheHeaders.Add("token2");
CheckValidTryParse("no-cache=\"token1, token2\"", expected);
expected = new CacheControlHeaderValue();
expected.NoStore = true;
expected.MaxAge = new TimeSpan(0, 0, 125);
expected.MaxStale = true;
CheckValidTryParse(" no-store , max-age = 125, max-stale,", expected);
expected = new CacheControlHeaderValue();
expected.MinFresh = new TimeSpan(0, 0, 123);
expected.NoTransform = true;
expected.OnlyIfCached = true;
expected.Extensions.Add(new NameValueHeaderValue("custom"));
CheckValidTryParse("min-fresh=123, no-transform, only-if-cached, custom", expected);
expected = new CacheControlHeaderValue();
expected.Public = true;
expected.Private = true;
expected.PrivateHeaders.Add("token1");
expected.MustRevalidate = true;
expected.ProxyRevalidate = true;
expected.Extensions.Add(new NameValueHeaderValue("c", "d"));
expected.Extensions.Add(new NameValueHeaderValue("a", "b"));
CheckValidTryParse(",public, , private=\"token1\", must-revalidate, c=d, proxy-revalidate, a=b", expected);
expected = new CacheControlHeaderValue();
expected.Private = true;
expected.SharedMaxAge = new TimeSpan(0, 0, 1234567890);
expected.MaxAge = new TimeSpan(0, 0, 987654321);
CheckValidTryParse("s-maxage=1234567890, private, max-age = 987654321,", expected);
expected = new CacheControlHeaderValue();
expected.Extensions.Add(new NameValueHeaderValue("custom", ""));
CheckValidTryParse("custom=", expected);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
// Token-only values
[InlineData("no-store=15")]
[InlineData("no-store=")]
[InlineData("no-transform=a")]
[InlineData("no-transform=")]
[InlineData("only-if-cached=\"x\"")]
[InlineData("only-if-cached=")]
[InlineData("public=\"x\"")]
[InlineData("public=")]
[InlineData("must-revalidate=\"1\"")]
[InlineData("must-revalidate=")]
[InlineData("proxy-revalidate=x")]
[InlineData("proxy-revalidate=")]
// Token with optional field-name list
[InlineData("no-cache=")]
[InlineData("no-cache=token")]
[InlineData("no-cache=\"token")]
[InlineData("no-cache=\"\"")] // at least one token expected as value
[InlineData("private=")]
[InlineData("private=token")]
[InlineData("private=\"token")]
[InlineData("private=\",\"")] // at least one token expected as value
[InlineData("private=\"=\"")]
// Token with delta-seconds value
[InlineData("max-age")]
[InlineData("max-age=")]
[InlineData("max-age=a")]
[InlineData("max-age=\"1\"")]
[InlineData("max-age=1.5")]
[InlineData("max-stale=")]
[InlineData("max-stale=a")]
[InlineData("max-stale=\"1\"")]
[InlineData("max-stale=1.5")]
[InlineData("min-fresh")]
[InlineData("min-fresh=")]
[InlineData("min-fresh=a")]
[InlineData("min-fresh=\"1\"")]
[InlineData("min-fresh=1.5")]
[InlineData("s-maxage")]
[InlineData("s-maxage=")]
[InlineData("s-maxage=a")]
[InlineData("s-maxage=\"1\"")]
[InlineData("s-maxage=1.5")]
// Invalid Extension values
[InlineData("custom value")]
public void TryParse_DifferentInvalidScenarios_ReturnsFalse(string input)
{
CheckInvalidTryParse(input);
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
// Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue.
var expected = new CacheControlHeaderValue();
expected.NoStore = true;
expected.MinFresh = new TimeSpan(0, 2, 3);
CheckValidParse(" , no-store, min-fresh=123", expected);
expected = new CacheControlHeaderValue();
expected.MaxStale = true;
expected.NoCache = true;
expected.NoCacheHeaders.Add("t");
CheckValidParse("max-stale, no-cache=\"t\", ,,", expected);
expected = new CacheControlHeaderValue();
expected.Extensions.Add(new NameValueHeaderValue("custom"));
CheckValidParse("custom =", expected);
expected = new CacheControlHeaderValue();
expected.Extensions.Add(new NameValueHeaderValue("custom", ""));
CheckValidParse("custom =", expected);
}
[Fact]
public void Parse_SetOfInvalidValueStrings_Throws()
{
CheckInvalidParse(null);
CheckInvalidParse("");
CheckInvalidParse(" ");
CheckInvalidParse("no-cache,=");
CheckInvalidParse("max-age=123x");
CheckInvalidParse("=no-cache");
CheckInvalidParse("no-cache no-store");
CheckInvalidParse("会");
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
// Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue.
var expected = new CacheControlHeaderValue();
expected.NoStore = true;
expected.MinFresh = new TimeSpan(0, 2, 3);
CheckValidTryParse(" , no-store, min-fresh=123", expected);
expected = new CacheControlHeaderValue();
expected.MaxStale = true;
expected.NoCache = true;
expected.NoCacheHeaders.Add("t");
CheckValidTryParse("max-stale, no-cache=\"t\", ,,", expected);
expected = new CacheControlHeaderValue();
expected.Extensions.Add(new NameValueHeaderValue("custom"));
CheckValidTryParse("custom = ", expected);
expected = new CacheControlHeaderValue();
expected.Extensions.Add(new NameValueHeaderValue("custom", ""));
CheckValidTryParse("custom =", expected);
}
[Fact]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
{
CheckInvalidTryParse("no-cache,=");
CheckInvalidTryParse("max-age=123x");
CheckInvalidTryParse("=no-cache");
CheckInvalidTryParse("no-cache no-store");
CheckInvalidTryParse("会");
}
#region Helper methods
private void CompareHashCodes(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual)
{
if (areEqual)
{
Assert.Equal(x.GetHashCode(), y.GetHashCode());
}
else
{
Assert.NotEqual(x.GetHashCode(), y.GetHashCode());
}
}
private void CompareValues(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual)
{
Assert.Equal(areEqual, x.Equals(y));
Assert.Equal(areEqual, y.Equals(x));
}
private void CheckValidParse(string input, CacheControlHeaderValue expectedResult)
{
var result = CacheControlHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckInvalidParse(string input)
{
Assert.Throws<FormatException>(() => CacheControlHeaderValue.Parse(input));
}
private void CheckValidTryParse(string input, CacheControlHeaderValue expectedResult)
{
CacheControlHeaderValue result = null;
Assert.True(CacheControlHeaderValue.TryParse(input, out result));
Assert.Equal(expectedResult, result);
}
private void CheckInvalidTryParse(string input)
{
CacheControlHeaderValue result = null;
Assert.False(CacheControlHeaderValue.TryParse(input, out result));
Assert.Null(result);
}
#endregion
}
}

View File

@ -0,0 +1,622 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class ContentDispositionHeaderValueTest
{
[Fact]
public void Ctor_ContentDispositionNull_Throw()
{
Assert.Throws<ArgumentException>(() => new ContentDispositionHeaderValue(null));
}
[Fact]
public void Ctor_ContentDispositionEmpty_Throw()
{
// null and empty should be treated the same. So we also throw for empty strings.
Assert.Throws<ArgumentException>(() => new ContentDispositionHeaderValue(string.Empty));
}
[Fact]
public void Ctor_ContentDispositionInvalidFormat_ThrowFormatException()
{
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
AssertFormatException(" inline ");
AssertFormatException(" inline");
AssertFormatException("inline ");
AssertFormatException("\"inline\"");
AssertFormatException("te xt");
AssertFormatException("te=xt");
AssertFormatException("teäxt");
AssertFormatException("text;");
AssertFormatException("te/xt;");
AssertFormatException("inline; name=someName; ");
AssertFormatException("text;name=someName"); // ctor takes only disposition-type name, no parameters
}
[Fact]
public void Ctor_ContentDispositionValidFormat_SuccessfullyCreated()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
Assert.Equal("inline", contentDisposition.DispositionType);
Assert.Equal(0, contentDisposition.Parameters.Count);
Assert.Null(contentDisposition.Name.Value);
Assert.Null(contentDisposition.FileName.Value);
Assert.Null(contentDisposition.CreationDate);
Assert.Null(contentDisposition.ModificationDate);
Assert.Null(contentDisposition.ReadDate);
Assert.Null(contentDisposition.Size);
}
[Fact]
public void Parameters_AddNull_Throw()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
Assert.Throws<ArgumentNullException>(() => contentDisposition.Parameters.Add(null));
}
[Fact]
public void ContentDisposition_SetAndGetContentDisposition_MatchExpectations()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
Assert.Equal("inline", contentDisposition.DispositionType);
contentDisposition.DispositionType = "attachment";
Assert.Equal("attachment", contentDisposition.DispositionType);
}
[Fact]
public void Name_SetNameAndValidateObject_ParametersEntryForNameAdded()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.Name = "myname";
Assert.Equal("myname", contentDisposition.Name);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("name", contentDisposition.Parameters.First().Name);
contentDisposition.Name = null;
Assert.Null(contentDisposition.Name.Value);
Assert.Equal(0, contentDisposition.Parameters.Count);
contentDisposition.Name = null; // It's OK to set it again to null; no exception.
}
[Fact]
public void Name_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
NameValueHeaderValue name = new NameValueHeaderValue("NAME", "old_name");
contentDisposition.Parameters.Add(name);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("NAME", contentDisposition.Parameters.First().Name);
contentDisposition.Name = "new_name";
Assert.Equal("new_name", contentDisposition.Name);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("NAME", contentDisposition.Parameters.First().Name);
contentDisposition.Parameters.Remove(name);
Assert.Null(contentDisposition.Name.Value);
}
[Fact]
public void FileName_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var fileName = new NameValueHeaderValue("FILENAME", "old_name");
contentDisposition.Parameters.Add(fileName);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name);
contentDisposition.FileName = "new_name";
Assert.Equal("new_name", contentDisposition.FileName);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name);
contentDisposition.Parameters.Remove(fileName);
Assert.Null(contentDisposition.FileName.Value);
}
[Fact]
public void FileName_NeedsEncoding_EncodedAndDecodedCorrectly()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.FileName = "FileÃName.bat";
Assert.Equal("FileÃName.bat", contentDisposition.FileName);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("filename", contentDisposition.Parameters.First().Name);
Assert.Equal("\"=?utf-8?B?RmlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value);
contentDisposition.Parameters.Remove(contentDisposition.Parameters.First());
Assert.Null(contentDisposition.FileName.Value);
}
[Fact]
public void FileName_UnknownOrBadEncoding_PropertyFails()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var fileName = new NameValueHeaderValue("FILENAME", "\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"");
contentDisposition.Parameters.Add(fileName);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name);
Assert.Equal("\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value);
Assert.Equal("=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=", contentDisposition.FileName);
contentDisposition.FileName = "new_name";
Assert.Equal("new_name", contentDisposition.FileName);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name);
contentDisposition.Parameters.Remove(fileName);
Assert.Null(contentDisposition.FileName.Value);
}
[Fact]
public void FileNameStar_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var fileNameStar = new NameValueHeaderValue("FILENAME*", "old_name");
contentDisposition.Parameters.Add(fileNameStar);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name);
Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure
contentDisposition.FileNameStar = "new_name";
Assert.Equal("new_name", contentDisposition.FileNameStar);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name);
Assert.Equal("UTF-8\'\'new_name", contentDisposition.Parameters.First().Value);
contentDisposition.Parameters.Remove(fileNameStar);
Assert.Null(contentDisposition.FileNameStar.Value);
}
[Fact]
public void FileNameStar_NeedsEncoding_EncodedAndDecodedCorrectly()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.FileNameStar = "FileÃName.bat";
Assert.Equal("FileÃName.bat", contentDisposition.FileNameStar);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("filename*", contentDisposition.Parameters.First().Name);
Assert.Equal("UTF-8\'\'File%C3%83Name.bat", contentDisposition.Parameters.First().Value);
contentDisposition.Parameters.Remove(contentDisposition.Parameters.First());
Assert.Null(contentDisposition.FileNameStar.Value);
}
[Fact]
public void FileNameStar_UnknownOrBadEncoding_PropertyFails()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var fileNameStar = new NameValueHeaderValue("FILENAME*", "utf-99'lang'File%CZName.bat");
contentDisposition.Parameters.Add(fileNameStar);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name);
Assert.Equal("utf-99'lang'File%CZName.bat", contentDisposition.Parameters.First().Value);
Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure
contentDisposition.FileNameStar = "new_name";
Assert.Equal("new_name", contentDisposition.FileNameStar);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name);
contentDisposition.Parameters.Remove(fileNameStar);
Assert.Null(contentDisposition.FileNameStar.Value);
}
[Fact]
public void Dates_AddDateParameterThenUseProperty_ParametersEntryIsOverwritten()
{
string validDateString = "\"Tue, 15 Nov 1994 08:12:31 GMT\"";
DateTimeOffset validDate = DateTimeOffset.Parse("Tue, 15 Nov 1994 08:12:31 GMT");
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var dateParameter = new NameValueHeaderValue("Creation-DATE", validDateString);
contentDisposition.Parameters.Add(dateParameter);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name);
Assert.Equal(validDate, contentDisposition.CreationDate);
var newDate = validDate.AddSeconds(1);
contentDisposition.CreationDate = newDate;
Assert.Equal(newDate, contentDisposition.CreationDate);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name);
Assert.Equal("\"Tue, 15 Nov 1994 08:12:32 GMT\"", contentDisposition.Parameters.First().Value);
contentDisposition.Parameters.Remove(dateParameter);
Assert.Null(contentDisposition.CreationDate);
}
[Fact]
public void Dates_InvalidDates_PropertyFails()
{
string invalidDateString = "\"Tue, 15 Nov 94 08:12 GMT\"";
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var dateParameter = new NameValueHeaderValue("read-DATE", invalidDateString);
contentDisposition.Parameters.Add(dateParameter);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("read-DATE", contentDisposition.Parameters.First().Name);
Assert.Null(contentDisposition.ReadDate);
contentDisposition.ReadDate = null;
Assert.Null(contentDisposition.ReadDate);
Assert.Equal(0, contentDisposition.Parameters.Count);
}
[Fact]
public void Size_AddSizeParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var sizeParameter = new NameValueHeaderValue("SIZE", "279172874239");
contentDisposition.Parameters.Add(sizeParameter);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name);
Assert.Equal(279172874239, contentDisposition.Size);
contentDisposition.Size = 279172874240;
Assert.Equal(279172874240, contentDisposition.Size);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name);
contentDisposition.Parameters.Remove(sizeParameter);
Assert.Null(contentDisposition.Size);
}
[Fact]
public void Size_InvalidSizes_PropertyFails()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var sizeParameter = new NameValueHeaderValue("SIZE", "-279172874239");
contentDisposition.Parameters.Add(sizeParameter);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name);
Assert.Null(contentDisposition.Size);
// Negatives not allowed
Assert.Throws<ArgumentOutOfRangeException>(() => contentDisposition.Size = -279172874240);
Assert.Null(contentDisposition.Size);
Assert.Equal(1, contentDisposition.Parameters.Count);
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name);
contentDisposition.Parameters.Remove(sizeParameter);
Assert.Null(contentDisposition.Size);
}
[Fact]
public void ToString_UseDifferentContentDispositions_AllSerializedCorrectly()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
Assert.Equal("inline", contentDisposition.ToString());
contentDisposition.Name = "myname";
Assert.Equal("inline; name=myname", contentDisposition.ToString());
contentDisposition.FileName = "my File Name";
Assert.Equal("inline; name=myname; filename=\"my File Name\"", contentDisposition.ToString());
contentDisposition.CreationDate = new DateTimeOffset(new DateTime(2011, 2, 15), new TimeSpan(-8, 0, 0));
Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date="
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"", contentDisposition.ToString());
contentDisposition.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\""));
Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date="
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString());
contentDisposition.Name = null;
Assert.Equal("inline; filename=\"my File Name\"; creation-date="
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString());
contentDisposition.FileNameStar = "File%Name";
Assert.Equal("inline; filename=\"my File Name\"; creation-date="
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name",
contentDisposition.ToString());
contentDisposition.FileName = null;
Assert.Equal("inline; creation-date=\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\";"
+ " filename*=UTF-8\'\'File%25Name", contentDisposition.ToString());
contentDisposition.CreationDate = null;
Assert.Equal("inline; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name",
contentDisposition.ToString());
}
[Fact]
public void GetHashCode_UseContentDispositionWithAndWithoutParameters_SameOrDifferentHashCodes()
{
var contentDisposition1 = new ContentDispositionHeaderValue("inline");
var contentDisposition2 = new ContentDispositionHeaderValue("inline");
contentDisposition2.Name = "myname";
var contentDisposition3 = new ContentDispositionHeaderValue("inline");
contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value"));
var contentDisposition4 = new ContentDispositionHeaderValue("INLINE");
var contentDisposition5 = new ContentDispositionHeaderValue("INLINE");
contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME"));
Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition2.GetHashCode());
Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition3.GetHashCode());
Assert.NotEqual(contentDisposition2.GetHashCode(), contentDisposition3.GetHashCode());
Assert.Equal(contentDisposition1.GetHashCode(), contentDisposition4.GetHashCode());
Assert.Equal(contentDisposition2.GetHashCode(), contentDisposition5.GetHashCode());
}
[Fact]
public void Equals_UseContentDispositionWithAndWithoutParameters_EqualOrNotEqualNoExceptions()
{
var contentDisposition1 = new ContentDispositionHeaderValue("inline");
var contentDisposition2 = new ContentDispositionHeaderValue("inline");
contentDisposition2.Name = "myName";
var contentDisposition3 = new ContentDispositionHeaderValue("inline");
contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value"));
var contentDisposition4 = new ContentDispositionHeaderValue("INLINE");
var contentDisposition5 = new ContentDispositionHeaderValue("INLINE");
contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME"));
var contentDisposition6 = new ContentDispositionHeaderValue("INLINE");
contentDisposition6.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME"));
contentDisposition6.Parameters.Add(new NameValueHeaderValue("custom", "value"));
var contentDisposition7 = new ContentDispositionHeaderValue("attachment");
Assert.False(contentDisposition1.Equals(contentDisposition2), "No params vs. name.");
Assert.False(contentDisposition2.Equals(contentDisposition1), "name vs. no params.");
Assert.False(contentDisposition1.Equals(null), "No params vs. <null>.");
Assert.False(contentDisposition1.Equals(contentDisposition3), "No params vs. custom param.");
Assert.False(contentDisposition2.Equals(contentDisposition3), "name vs. custom param.");
Assert.True(contentDisposition1.Equals(contentDisposition4), "Different casing.");
Assert.True(contentDisposition2.Equals(contentDisposition5), "Different casing in name.");
Assert.False(contentDisposition5.Equals(contentDisposition6), "name vs. custom param.");
Assert.False(contentDisposition1.Equals(contentDisposition7), "inline vs. text/other.");
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
var expected = new ContentDispositionHeaderValue("inline");
CheckValidParse("\r\n inline ", expected);
CheckValidParse("inline", expected);
// We don't have to test all possible input strings, since most of the pieces are handled by other parsers.
// The purpose of this test is to verify that these other parsers are combined correctly to build a
// Content-Disposition parser.
expected.Name = "myName";
CheckValidParse("\r\n inline ; name = myName ", expected);
CheckValidParse(" inline;name=myName", expected);
expected.Name = null;
expected.DispositionType = "attachment";
expected.FileName = "foo-ae.html";
expected.Parameters.Add(new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4.html"));
CheckValidParse(@"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=foo-ae.html", expected);
}
[Fact]
public void Parse_SetOfInvalidValueStrings_Throws()
{
CheckInvalidParse("");
CheckInvalidParse(" ");
CheckInvalidParse(null);
CheckInvalidParse("inline会");
CheckInvalidParse("inline ,");
CheckInvalidParse("inline,");
CheckInvalidParse("inline; name=myName ,");
CheckInvalidParse("inline; name=myName,");
CheckInvalidParse("inline; name=my会Name");
CheckInvalidParse("inline/");
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
var expected = new ContentDispositionHeaderValue("inline");
CheckValidTryParse("\r\n inline ", expected);
CheckValidTryParse("inline", expected);
// We don't have to test all possible input strings, since most of the pieces are handled by other parsers.
// The purpose of this test is to verify that these other parsers are combined correctly to build a
// Content-Disposition parser.
expected.Name = "myName";
CheckValidTryParse("\r\n inline ; name = myName ", expected);
CheckValidTryParse(" inline;name=myName", expected);
}
[Fact]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
{
CheckInvalidTryParse("");
CheckInvalidTryParse(" ");
CheckInvalidTryParse(null);
CheckInvalidTryParse("inline会");
CheckInvalidTryParse("inline ,");
CheckInvalidTryParse("inline,");
CheckInvalidTryParse("inline; name=myName ,");
CheckInvalidTryParse("inline; name=myName,");
CheckInvalidTryParse("text/");
}
public static TheoryData<string, ContentDispositionHeaderValue> ValidContentDispositionTestCases = new TheoryData<string, ContentDispositionHeaderValue>()
{
{ "inline", new ContentDispositionHeaderValue("inline") }, // @"This should be equivalent to not including the header at all."
{ "inline;", new ContentDispositionHeaderValue("inline") },
{ "inline;name=", new ContentDispositionHeaderValue("inline") { Parameters = { new NameValueHeaderValue("name", "") } } }, // TODO: passing in a null value causes a strange assert on CoreCLR before the test even starts. Not reproducable in the body of a test.
{ "inline;name=value", new ContentDispositionHeaderValue("inline") { Name = "value" } },
{ "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } },
{ "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } },
{ @"inline; filename=""foo.html""", new ContentDispositionHeaderValue("inline") { FileName = @"""foo.html""" } },
{ @"inline; filename=""Not an attachment!""", new ContentDispositionHeaderValue("inline") { FileName = @"""Not an attachment!""" } }, // 'inline', specifying a filename of Not an attachment! - this checks for proper parsing for disposition types.
{ @"inline; filename=""foo.pdf""", new ContentDispositionHeaderValue("inline") { FileName = @"""foo.pdf""" } },
{ "attachment", new ContentDispositionHeaderValue("attachment") },
{ "ATTACHMENT", new ContentDispositionHeaderValue("ATTACHMENT") },
{ @"attachment; filename=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } },
{ @"attachment; filename=""\""quoting\"" tested.html""", new ContentDispositionHeaderValue("attachment") { FileName = "\"\"quoting\" tested.html\"" } }, // 'attachment', specifying a filename of \"quoting\" tested.html (using double quotes around "quoting" to test... quoting)
{ @"attachment; filename=""Here's a semicolon;.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""Here's a semicolon;.html""" } }, // , 'attachment', specifying a filename of Here's a semicolon;.html - this checks for proper parsing for parameters.
{ @"attachment; foo=""bar""; filename=""foo.html""", new ContentDispositionHeaderValue(@"attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foo", @"""bar""") } } }, // 'attachment', specifying a filename of foo.html and an extension parameter "foo" which should be ignored (see <a href="http://greenbytes.de/tech/webdav/rfc2183.html#rfc.section.2.8">Section 2.8 of RFC 2183</a>.).
{ @"attachment; foo=""\""\\"";filename=""foo.html""", new ContentDispositionHeaderValue(@"attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foo", @"""\""\\""") } } }, // 'attachment', specifying a filename of foo.html and an extension parameter "foo" which should be ignored (see <a href="http://greenbytes.de/tech/webdav/rfc2183.html#rfc.section.2.8">Section 2.8 of RFC 2183</a>.). The extension parameter actually uses backslash-escapes. This tests whether the UA properly skips the parameter.
{ @"attachment; FILENAME=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } },
{ @"attachment; filename=foo.html", new ContentDispositionHeaderValue("attachment") { FileName = "foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string.
{ @"attachment; filename='foo.bar'", new ContentDispositionHeaderValue("attachment") { FileName = "'foo.bar'" } }, // 'attachment', specifying a filename of 'foo.bar' using single quotes.
{ @"attachment; filename=""foo-ä.html""", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("filename", @"""foo-ä.html""") } } }, // 'attachment', specifying a filename of foo-ä.html, using plain ISO-8859-1
{ @"attachment; filename=""foo-&#xc3;&#xa4;.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-&#xc3;&#xa4;.html""" } }, // 'attachment', specifying a filename of foo-&#xc3;&#xa4;.html, which happens to be foo-ä.html using UTF-8 encoding.
{ @"attachment; filename=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%41.html""") } } },
{ @"attachment; filename=""50%.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""50%.html""") } } },
{ @"attachment; filename=""foo-%\41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%\41.html""") } } }, // 'attachment', specifying a filename of foo-%41.html, using an escape character (this tests whether adding an escape character inside a %xx sequence can be used to disable the non-conformant %xx-unescaping).
{ @"attachment; name=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Name = @"""foo-%41.html""" } }, // 'attachment', specifying a <i>name</i> parameter of foo-%41.html. (this test was added to observe the behavior of the (unspecified) treatment of ""name"" as synonym for ""filename""; see <a href=""http://www.imc.org/ietf-smtp/mail-archive/msg05023.html"">Ned Freed's summary</a> where this comes from in MIME messages)
{ @"attachment; filename=""ä-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""ä-%41.html""") } } }, // 'attachment', specifying a filename parameter of ä-%41.html. (this test was added to observe the behavior when non-ASCII characters and percent-hexdig sequences are combined)
{ @"attachment; filename=""foo-%c3%a4-%e2%82%ac.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-%c3%a4-%e2%82%ac.html""" } }, // 'attachment', specifying a filename of foo-%c3%a4-%e2%82%ac.html, using raw percent encoded UTF-8 to represent foo-ä-&#x20ac;.html
{ @"attachment; filename =""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } },
{ @"attachment; xfilename=foo.html", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("xfilename", "foo.html") } } },
{ @"attachment; filename=""/foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""/foo.html""" } },
{ @"attachment; creation-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("creation-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } },
{ @"attachment; modification-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("modification-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } },
{ @"foobar", new ContentDispositionHeaderValue("foobar") }, // @"This should be equivalent to using ""attachment""."
{ @"attachment; example=""filename=example.txt""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("example", @"""filename=example.txt""") } } },
{ @"attachment; filename*=iso-8859-1''foo-%E4.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "iso-8859-1''foo-%E4.html") } } }, // 'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded ISO-8859-1
{ @"attachment; filename*=UTF-8''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4-%e2%82%ac.html") } } }, // 'attachment', specifying a filename of foo-ä-&#x20ac;.html, using RFC2231 encoded UTF-8
{ @"attachment; filename*=''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "''foo-%c3%a4-%e2%82%ac.html") } } }, // Behavior is undefined in RFC 2231, the charset part is missing, although UTF-8 was used.
{ @"attachment; filename*=UTF-8''foo-a%22.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"foo-a"".html" } },
{ @"attachment; filename*= UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } },
{ @"attachment; filename* =UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } },
{ @"attachment; filename*=UTF-8''A-%2541.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "A-%41.html" } },
{ @"attachment; filename*=UTF-8''%5cfoo.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"\foo.html" } },
{ @"attachment; filename=""foo-ae.html""; filename*=UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ae.html""", FileNameStar = "foo-ä.html" } },
{ @"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=""foo-ae.html""", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html", FileName = @"""foo-ae.html""" } },
{ @"attachment; foobar=x; filename=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foobar", "x") } } },
{ @"attachment; filename=""=?ISO-8859-1?Q?foo-=E4.html?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?ISO-8859-1?Q?foo-=E4.html?=""" } }, // attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="
{ @"attachment; filename=""=?utf-8?B?Zm9vLeQuaHRtbA==?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?utf-8?B?Zm9vLeQuaHRtbA==?=""" } }, // attachment; filename="=?utf-8?B?Zm9vLeQuaHRtbA==?="
{ @"attachment; filename=foo.html ;", new ContentDispositionHeaderValue("attachment") { FileName="foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string, and adding a trailing semicolon.,
};
[Theory]
[MemberData(nameof(ValidContentDispositionTestCases))]
public void ContentDispositionHeaderValue_ParseValid_Success(string input, ContentDispositionHeaderValue expected)
{
// System.Diagnostics.Debugger.Launch();
var result = ContentDispositionHeaderValue.Parse(input);
Assert.Equal(expected, result);
}
[Theory]
// Invalid values
[InlineData(@"""inline""")] // @"'inline' only, using double quotes", false) },
[InlineData(@"""attachment""")] // @"'attachment' only, using double quotes", false) },
[InlineData(@"attachment; filename=foo bar.html")] // @"'attachment', specifying a filename of foo bar.html without using quoting.", false) },
// Duplicate file name parameter
// @"attachment; filename=""foo.html""; // filename=""bar.html""", @"'attachment', specifying two filename parameters. This is invalid syntax.", false) },
[InlineData(@"attachment; filename=foo[1](2).html")] // @"'attachment', specifying a filename of foo[1](2).html, but missing the quotes. Also, ""["", ""]"", ""("" and "")"" are not allowed in the HTTP <a href=""http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p1-messaging-latest.html#rfc.section.1.2.2"">token</a> production.", false) },
[InlineData(@"attachment; filename=foo-ä.html")] // @"'attachment', specifying a filename of foo-ä.html, but missing the quotes.", false) },
// HTML escaping, not supported
// @"attachment; filename=foo-&#xc3;&#xa4;.html", // "'attachment', specifying a filename of foo-&#xc3;&#xa4;.html (which happens to be foo-ä.html using UTF-8 encoding) but missing the quotes.", false) },
[InlineData(@"filename=foo.html")] // @"Disposition type missing, filename specified.", false) },
[InlineData(@"x=y; filename=foo.html")] // @"Disposition type missing, filename specified after extension parameter.", false) },
[InlineData(@"""foo; filename=bar;baz""; filename=qux")] // @"Disposition type missing, filename ""qux"". Can it be more broken? (Probably)", false) },
[InlineData(@"filename=foo.html, filename=bar.html")] // @"Disposition type missing, two filenames specified separated by a comma (this is syntactically equivalent to have two instances of the header with one filename parameter each).", false) },
[InlineData(@"; filename=foo.html")] // @"Disposition type missing (but delimiter present), filename specified.", false) },
// This is permitted as a parameter without a value
// @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) },
// This is permitted as a parameter without a value
// @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) },
[InlineData(@"attachment; filename=""foo.html"".txt")] // @"'attachment', specifying a filename parameter that is broken (quoted-string followed by more characters). This is invalid syntax. ", false) },
[InlineData(@"attachment; filename=""bar")] // @"'attachment', specifying a filename parameter that is broken (missing ending double quote). This is invalid syntax.", false) },
[InlineData(@"attachment; filename=foo""bar;baz""qux")] // @"'attachment', specifying a filename parameter that is broken (disallowed characters in token syntax). This is invalid syntax.", false) },
[InlineData(@"attachment; filename=foo.html, attachment; filename=bar.html")] // @"'attachment', two comma-separated instances of the header field. As Content-Disposition doesn't use a list-style syntax, this is invalid syntax and, according to <a href=""http://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.4.2.p.5"">RFC 2616, Section 4.2</a>, roughly equivalent to having two separate header field instances.", false) },
[InlineData(@"filename=foo.html; attachment")] // @"filename parameter and disposition type reversed.", false) },
// Escaping is not verified
// @"attachment; filename*=iso-8859-1''foo-%c3%a4-%e2%82%ac.html", // @"'attachment', specifying a filename of foo-ä-&#x20ac;.html, using RFC2231 encoded UTF-8, but declaring ISO-8859-1", false) },
// Escaping is not verified
// @"attachment; filename *=UTF-8''foo-%c3%a4.html", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with whitespace before ""*=""", false) },
// Escaping is not verified
// @"attachment; filename*=""UTF-8''foo-%c3%a4.html""", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with double quotes around the parameter value.", false) },
[InlineData(@"attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) },
[InlineData(@"attachment; filename==?utf-8?B?Zm9vLeQuaHRtbA==?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) },
public void ContentDispositionHeaderValue_ParseInvalid_Throws(string input)
{
Assert.Throws<FormatException>(() => ContentDispositionHeaderValue.Parse(input));
}
[Fact]
public void HeaderNamesWithQuotes_ExpectNamesToNotHaveQuotes()
{
var contentDispositionLine = "form-data; name =\"dotnet\"; filename=\"example.png\"";
var expectedName = "dotnet";
var expectedFileName = "example.png";
var result = ContentDispositionHeaderValue.Parse(contentDispositionLine);
Assert.Equal(expectedName, result.Name);
Assert.Equal(expectedFileName, result.FileName);
}
public class ContentDispositionValue
{
public ContentDispositionValue(string value, string description, bool valid)
{
Value = value;
Description = description;
Valid = valid;
}
public string Value { get; }
public string Description { get; }
public bool Valid { get; }
}
private void CheckValidParse(string input, ContentDispositionHeaderValue expectedResult)
{
var result = ContentDispositionHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckInvalidParse(string input)
{
Assert.Throws<FormatException>(() => ContentDispositionHeaderValue.Parse(input));
}
private void CheckValidTryParse(string input, ContentDispositionHeaderValue expectedResult)
{
ContentDispositionHeaderValue result = null;
Assert.True(ContentDispositionHeaderValue.TryParse(input, out result), input);
Assert.Equal(expectedResult, result);
}
private void CheckInvalidTryParse(string input)
{
ContentDispositionHeaderValue result = null;
Assert.False(ContentDispositionHeaderValue.TryParse(input, out result), input);
Assert.Null(result);
}
private static void AssertFormatException(string contentDisposition)
{
Assert.Throws<FormatException>(() => new ContentDispositionHeaderValue(contentDisposition));
}
}
}

View File

@ -0,0 +1,272 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class ContentRangeHeaderValueTest
{
[Fact]
public void Ctor_LengthOnlyOverloadUseInvalidValues_Throw()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(-1));
}
[Fact]
public void Ctor_LengthOnlyOverloadValidValues_ValuesCorrectlySet()
{
var range = new ContentRangeHeaderValue(5);
Assert.False(range.HasRange, "HasRange");
Assert.True(range.HasLength, "HasLength");
Assert.Equal("bytes", range.Unit);
Assert.Null(range.From);
Assert.Null(range.To);
Assert.Equal(5, range.Length);
}
[Fact]
public void Ctor_FromAndToOverloadUseInvalidValues_Throw()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(-1, 1));
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(0, -1));
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(2, 1));
}
[Fact]
public void Ctor_FromAndToOverloadValidValues_ValuesCorrectlySet()
{
var range = new ContentRangeHeaderValue(0, 1);
Assert.True(range.HasRange, "HasRange");
Assert.False(range.HasLength, "HasLength");
Assert.Equal("bytes", range.Unit);
Assert.Equal(0, range.From);
Assert.Equal(1, range.To);
Assert.Null(range.Length);
}
[Fact]
public void Ctor_FromToAndLengthOverloadUseInvalidValues_Throw()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(-1, 1, 2));
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(0, -1, 2));
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(0, 1, -1));
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(2, 1, 3));
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(1, 2, 1));
}
[Fact]
public void Ctor_FromToAndLengthOverloadValidValues_ValuesCorrectlySet()
{
var range = new ContentRangeHeaderValue(0, 1, 2);
Assert.True(range.HasRange, "HasRange");
Assert.True(range.HasLength, "HasLength");
Assert.Equal("bytes", range.Unit);
Assert.Equal(0, range.From);
Assert.Equal(1, range.To);
Assert.Equal(2, range.Length);
}
[Fact]
public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation()
{
var range = new ContentRangeHeaderValue(0);
range.Unit = "myunit";
Assert.Equal("myunit", range.Unit);
Assert.Throws<ArgumentException>(() => range.Unit = null);
Assert.Throws<ArgumentException>(() => range.Unit = "");
Assert.Throws<FormatException>(() => range.Unit = " x");
Assert.Throws<FormatException>(() => range.Unit = "x ");
Assert.Throws<FormatException>(() => range.Unit = "x y");
}
[Fact]
public void ToString_UseDifferentRanges_AllSerializedCorrectly()
{
var range = new ContentRangeHeaderValue(1, 2, 3);
range.Unit = "myunit";
Assert.Equal("myunit 1-2/3", range.ToString());
range = new ContentRangeHeaderValue(123456789012345678, 123456789012345679);
Assert.Equal("bytes 123456789012345678-123456789012345679/*", range.ToString());
range = new ContentRangeHeaderValue(150);
Assert.Equal("bytes */150", range.ToString());
}
[Fact]
public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes()
{
var range1 = new ContentRangeHeaderValue(1, 2, 5);
var range2 = new ContentRangeHeaderValue(1, 2);
var range3 = new ContentRangeHeaderValue(5);
var range4 = new ContentRangeHeaderValue(1, 2, 5);
range4.Unit = "BYTES";
var range5 = new ContentRangeHeaderValue(1, 2, 5);
range5.Unit = "myunit";
Assert.NotEqual(range1.GetHashCode(), range2.GetHashCode());
Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode());
Assert.NotEqual(range2.GetHashCode(), range3.GetHashCode());
Assert.Equal(range1.GetHashCode(), range4.GetHashCode());
Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode());
}
[Fact]
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
{
var range1 = new ContentRangeHeaderValue(1, 2, 5);
var range2 = new ContentRangeHeaderValue(1, 2);
var range3 = new ContentRangeHeaderValue(5);
var range4 = new ContentRangeHeaderValue(1, 2, 5);
range4.Unit = "BYTES";
var range5 = new ContentRangeHeaderValue(1, 2, 5);
range5.Unit = "myunit";
var range6 = new ContentRangeHeaderValue(1, 3, 5);
var range7 = new ContentRangeHeaderValue(2, 2, 5);
var range8 = new ContentRangeHeaderValue(1, 2, 6);
Assert.False(range1.Equals(null), "bytes 1-2/5 vs. <null>");
Assert.False(range1.Equals(range2), "bytes 1-2/5 vs. bytes 1-2/*");
Assert.False(range1.Equals(range3), "bytes 1-2/5 vs. bytes */5");
Assert.False(range2.Equals(range3), "bytes 1-2/* vs. bytes */5");
Assert.True(range1.Equals(range4), "bytes 1-2/5 vs. BYTES 1-2/5");
Assert.True(range4.Equals(range1), "BYTES 1-2/5 vs. bytes 1-2/5");
Assert.False(range1.Equals(range5), "bytes 1-2/5 vs. myunit 1-2/5");
Assert.False(range1.Equals(range6), "bytes 1-2/5 vs. bytes 1-3/5");
Assert.False(range1.Equals(range7), "bytes 1-2/5 vs. bytes 2-2/5");
Assert.False(range1.Equals(range8), "bytes 1-2/5 vs. bytes 1-2/6");
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3));
CheckValidParse("bytes * / 3", new ContentRangeHeaderValue(3));
CheckValidParse(" custom 1234567890123456789-1234567890123456799/*",
new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" });
CheckValidParse(" custom * / 123 ",
new ContentRangeHeaderValue(123) { Unit = "custom" });
// Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a
// scenario for it. However, if a server returns this value, we're flexible and accept it.
var result = ContentRangeHeaderValue.Parse("bytes */*");
Assert.Equal("bytes", result.Unit);
Assert.Null(result.From);
Assert.Null(result.To);
Assert.Null(result.Length);
Assert.False(result.HasRange, "HasRange");
Assert.False(result.HasLength, "HasLength");
}
[Theory]
[InlineData("bytes 1-2/3,")] // no character after 'length' allowed
[InlineData("x bytes 1-2/3")]
[InlineData("bytes 1-2/3.4")]
[InlineData(null)]
[InlineData("")]
[InlineData("bytes 3-2/5")]
[InlineData("bytes 6-6/5")]
[InlineData("bytes 1-6/5")]
[InlineData("bytes 1-2/")]
[InlineData("bytes 1-2")]
[InlineData("bytes 1-/")]
[InlineData("bytes 1-")]
[InlineData("bytes 1")]
[InlineData("bytes ")]
[InlineData("bytes a-2/3")]
[InlineData("bytes 1-b/3")]
[InlineData("bytes 1-2/c")]
[InlineData("bytes1-2/3")]
// More than 19 digits >>Int64.MaxValue
[InlineData("bytes 1-12345678901234567890/3")]
[InlineData("bytes 12345678901234567890-3/3")]
[InlineData("bytes 1-2/12345678901234567890")]
// Exceed Int64.MaxValue, but use 19 digits
[InlineData("bytes 1-9999999999999999999/3")]
[InlineData("bytes 9999999999999999999-3/3")]
[InlineData("bytes 1-2/9999999999999999999")]
public void Parse_SetOfInvalidValueStrings_Throws(string input)
{
Assert.Throws<FormatException>(() => ContentRangeHeaderValue.Parse(input));
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidTryParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3));
CheckValidTryParse("bytes * / 3", new ContentRangeHeaderValue(3));
CheckValidTryParse(" custom 1234567890123456789-1234567890123456799/*",
new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" });
CheckValidTryParse(" custom * / 123 ",
new ContentRangeHeaderValue(123) { Unit = "custom" });
// Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a
// scenario for it. However, if a server returns this value, we're flexible and accept it.
ContentRangeHeaderValue result = null;
Assert.True(ContentRangeHeaderValue.TryParse("bytes */*", out result));
Assert.Equal("bytes", result.Unit);
Assert.Null(result.From);
Assert.Null(result.To);
Assert.Null(result.Length);
Assert.False(result.HasRange, "HasRange");
Assert.False(result.HasLength, "HasLength");
}
[Theory]
[InlineData("bytes 1-2/3,")] // no character after 'length' allowed
[InlineData("x bytes 1-2/3")]
[InlineData("bytes 1-2/3.4")]
[InlineData(null)]
[InlineData("")]
[InlineData("bytes 3-2/5")]
[InlineData("bytes 6-6/5")]
[InlineData("bytes 1-6/5")]
[InlineData("bytes 1-2/")]
[InlineData("bytes 1-2")]
[InlineData("bytes 1-/")]
[InlineData("bytes 1-")]
[InlineData("bytes 1")]
[InlineData("bytes ")]
[InlineData("bytes a-2/3")]
[InlineData("bytes 1-b/3")]
[InlineData("bytes 1-2/c")]
[InlineData("bytes1-2/3")]
// More than 19 digits >>Int64.MaxValue
[InlineData("bytes 1-12345678901234567890/3")]
[InlineData("bytes 12345678901234567890-3/3")]
[InlineData("bytes 1-2/12345678901234567890")]
// Exceed Int64.MaxValue, but use 19 digits
[InlineData("bytes 1-9999999999999999999/3")]
[InlineData("bytes 9999999999999999999-3/3")]
[InlineData("bytes 1-2/9999999999999999999")]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input)
{
ContentRangeHeaderValue result = null;
Assert.False(ContentRangeHeaderValue.TryParse(input, out result));
Assert.Null(result);
}
private void CheckValidParse(string input, ContentRangeHeaderValue expectedResult)
{
var result = ContentRangeHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckValidTryParse(string input, ContentRangeHeaderValue expectedResult)
{
ContentRangeHeaderValue result = null;
Assert.True(ContentRangeHeaderValue.TryParse(input, out result));
Assert.Equal(expectedResult, result);
}
}
}

View File

@ -0,0 +1,326 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class CookieHeaderValueTest
{
public static TheoryData<CookieHeaderValue, string> CookieHeaderDataSet
{
get
{
var dataset = new TheoryData<CookieHeaderValue, string>();
var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3");
dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3");
var header2 = new CookieHeaderValue("name2", "");
dataset.Add(header2, "name2=");
var header3 = new CookieHeaderValue("name3", "value3");
dataset.Add(header3, "name3=value3");
var header4 = new CookieHeaderValue("name4", "\"value4\"");
dataset.Add(header4, "name4=\"value4\"");
return dataset;
}
}
public static TheoryData<string> InvalidCookieHeaderDataSet
{
get
{
return new TheoryData<string>
{
"=value",
"name=value;",
"name=value,",
};
}
}
public static TheoryData<string> InvalidCookieNames
{
get
{
return new TheoryData<string>
{
"<acb>",
"{acb}",
"[acb]",
"\"acb\"",
"a,b",
"a;b",
"a\\b",
"a b",
};
}
}
public static TheoryData<string> InvalidCookieValues
{
get
{
return new TheoryData<string>
{
{ "\"" },
{ "a,b" },
{ "a;b" },
{ "a\\b" },
{ "\"abc" },
{ "a\"bc" },
{ "abc\"" },
{ "a b" },
};
}
}
public static TheoryData<IList<CookieHeaderValue>, string[]> ListOfCookieHeaderDataSet
{
get
{
var dataset = new TheoryData<IList<CookieHeaderValue>, string[]>();
var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3");
var string1 = "name1=n1=v1&n2=v2&n3=v3";
var header2 = new CookieHeaderValue("name2", "value2");
var string2 = "name2=value2";
var header3 = new CookieHeaderValue("name3", "value3");
var string3 = "name3=value3";
var header4 = new CookieHeaderValue("name4", "\"value4\"");
var string4 = "name4=\"value4\"";
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ";", " , ", string1 });
dataset.Add(new[] { header2 }.ToList(), new[] { string2 });
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 });
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 });
dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + "; " + string1 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(";", string1, string2, string3, string4) });
return dataset;
}
}
public static TheoryData<IList<CookieHeaderValue>, string[]> ListWithInvalidCookieHeaderDataSet
{
get
{
var dataset = new TheoryData<IList<CookieHeaderValue>, string[]>();
var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3");
var validString1 = "name1=n1=v1&n2=v2&n3=v3";
var header2 = new CookieHeaderValue("name2", "value2");
var validString2 = "name2=value2";
var header3 = new CookieHeaderValue("name3", "value3");
var validString3 = "name3=value3";
var invalidString1 = "ipt={\"v\":{\"L\":3},\"pt\":{\"d\":3},ct\":{},\"_t\":44,\"_v\":\"2\"}";
dataset.Add(null, new[] { invalidString1 });
dataset.Add(new[] { header1 }.ToList(), new[] { validString1, invalidString1 });
dataset.Add(new[] { header1 }.ToList(), new[] { validString1, null, "", " ", ";", " , ", invalidString1 });
dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ";", " , ", validString1 });
dataset.Add(new[] { header1 }.ToList(), new[] { validString1 + ", " + invalidString1 });
dataset.Add(new[] { header2 }.ToList(), new[] { invalidString1 + ", " + validString2 });
dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + "; " + validString1 });
dataset.Add(new[] { header2 }.ToList(), new[] { validString2 + "; " + invalidString1 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { invalidString1, validString1, validString2, validString3 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, invalidString1, validString2, validString3 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, invalidString1, validString3 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, validString3, invalidString1 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", invalidString1, validString1, validString2, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, invalidString1, validString2, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, invalidString1, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, validString3, invalidString1) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", invalidString1, validString1, validString2, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, invalidString1, validString2, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, invalidString1, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, validString3, invalidString1) });
return dataset;
}
}
[Fact]
public void CookieHeaderValue_CtorThrowsOnNullName()
{
Assert.Throws<ArgumentNullException>(() => new CookieHeaderValue(null, "value"));
}
[Theory]
[MemberData(nameof(InvalidCookieNames))]
public void CookieHeaderValue_CtorThrowsOnInvalidName(string name)
{
Assert.Throws<ArgumentException>(() => new CookieHeaderValue(name, "value"));
}
[Theory]
[MemberData(nameof(InvalidCookieValues))]
public void CookieHeaderValue_CtorThrowsOnInvalidValue(string value)
{
Assert.Throws<ArgumentException>(() => new CookieHeaderValue("name", value));
}
[Fact]
public void CookieHeaderValue_Ctor1_InitializesCorrectly()
{
var header = new CookieHeaderValue("cookie");
Assert.Equal("cookie", header.Name);
Assert.Equal(string.Empty, header.Value);
}
[Theory]
[InlineData("name", "")]
[InlineData("name", "value")]
[InlineData("name", "\"acb\"")]
public void CookieHeaderValue_Ctor2InitializesCorrectly(string name, string value)
{
var header = new CookieHeaderValue(name, value);
Assert.Equal(name, header.Name);
Assert.Equal(value, header.Value);
}
[Fact]
public void CookieHeaderValue_Value()
{
var cookie = new CookieHeaderValue("name");
Assert.Equal(string.Empty, cookie.Value);
cookie.Value = "value1";
Assert.Equal("value1", cookie.Value);
}
[Theory]
[MemberData(nameof(CookieHeaderDataSet))]
public void CookieHeaderValue_ToString(CookieHeaderValue input, string expectedValue)
{
Assert.Equal(expectedValue, input.ToString());
}
[Theory]
[MemberData(nameof(CookieHeaderDataSet))]
public void CookieHeaderValue_Parse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue)
{
var header = CookieHeaderValue.Parse(expectedValue);
Assert.Equal(cookie, header);
Assert.Equal(expectedValue, header.ToString());
}
[Theory]
[MemberData(nameof(CookieHeaderDataSet))]
public void CookieHeaderValue_TryParse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue)
{
Assert.True(CookieHeaderValue.TryParse(expectedValue, out var header));
Assert.Equal(cookie, header);
Assert.Equal(expectedValue, header.ToString());
}
[Theory]
[MemberData(nameof(InvalidCookieHeaderDataSet))]
public void CookieHeaderValue_Parse_RejectsInvalidValues(string value)
{
Assert.Throws<FormatException>(() => CookieHeaderValue.Parse(value));
}
[Theory]
[MemberData(nameof(InvalidCookieHeaderDataSet))]
public void CookieHeaderValue_TryParse_RejectsInvalidValues(string value)
{
Assert.False(CookieHeaderValue.TryParse(value, out var _));
}
[Theory]
[MemberData(nameof(ListOfCookieHeaderDataSet))]
public void CookieHeaderValue_ParseList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input)
{
var results = CookieHeaderValue.ParseList(input);
Assert.Equal(cookies, results);
}
[Theory]
[MemberData(nameof(ListOfCookieHeaderDataSet))]
public void CookieHeaderValue_ParseStrictList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input)
{
var results = CookieHeaderValue.ParseStrictList(input);
Assert.Equal(cookies, results);
}
[Theory]
[MemberData(nameof(ListOfCookieHeaderDataSet))]
public void CookieHeaderValue_TryParseList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input)
{
var result = CookieHeaderValue.TryParseList(input, out var results);
Assert.True(result);
Assert.Equal(cookies, results);
}
[Theory]
[MemberData(nameof(ListOfCookieHeaderDataSet))]
public void CookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input)
{
var result = CookieHeaderValue.TryParseStrictList(input, out var results);
Assert.True(result);
Assert.Equal(cookies, results);
}
[Theory]
[MemberData(nameof(ListWithInvalidCookieHeaderDataSet))]
public void CookieHeaderValue_ParseList_ExcludesInvalidValues(IList<CookieHeaderValue> cookies, string[] input)
{
var results = CookieHeaderValue.ParseList(input);
// ParseList aways returns a list, even if empty. TryParseList may return null (via out).
Assert.Equal(cookies ?? new List<CookieHeaderValue>(), results);
}
[Theory]
[MemberData(nameof(ListWithInvalidCookieHeaderDataSet))]
public void CookieHeaderValue_TryParseList_ExcludesInvalidValues(IList<CookieHeaderValue> cookies, string[] input)
{
var result = CookieHeaderValue.TryParseList(input, out var results);
Assert.Equal(cookies, results);
Assert.Equal(cookies?.Count > 0, result);
}
[Theory]
[MemberData(nameof(ListWithInvalidCookieHeaderDataSet))]
public void CookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues(
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
IList<CookieHeaderValue> cookies,
#pragma warning restore xUnit1026 // Theory methods should use all of their parameters
string[] input)
{
Assert.Throws<FormatException>(() => CookieHeaderValue.ParseStrictList(input));
}
[Theory]
[MemberData(nameof(ListWithInvalidCookieHeaderDataSet))]
public void CookieHeaderValue_TryParseStrictList_FailsForAnyInvalidValues(
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
IList<CookieHeaderValue> cookies,
#pragma warning restore xUnit1026 // Theory methods should use all of their parameters
string[] input)
{
var result = CookieHeaderValue.TryParseStrictList(input, out var results);
Assert.Null(results);
Assert.False(result);
}
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class DateParserTest
{
[Theory]
[MemberData(nameof(ValidStringData))]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly(string input, DateTimeOffset expected)
{
// We don't need to validate all possible date values, since they're already tested in HttpRuleParserTest.
// Just make sure the parser calls HttpRuleParser methods correctly.
Assert.True(HeaderUtilities.TryParseDate(input, out var result));
Assert.Equal(expected, result);
}
public static IEnumerable<object[]> ValidStringData()
{
yield return new object[] { "Tue, 15 Nov 1994 08:12:31 GMT", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero) };
yield return new object[] { " Sunday, 06-Nov-94 08:49:37 GMT ", new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero) };
yield return new object[] { " Tue,\r\n 15 Nov\r\n 1994 08:12:31 GMT ", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero) };
yield return new object[] { "Sat, 09-Dec-2017 07:07:03 GMT ", new DateTimeOffset(2017, 12, 09, 7, 7, 3, TimeSpan.Zero) };
}
[Theory]
[MemberData(nameof(InvalidStringData))]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input)
{
Assert.False(HeaderUtilities.TryParseDate(input, out var result));
Assert.Equal(new DateTimeOffset(), result);
}
public static IEnumerable<object[]> InvalidStringData()
{
yield return new object[] { null };
yield return new object[] { string.Empty };
yield return new object[] { " " };
yield return new object[] { "!!Sunday, 06-Nov-94 08:49:37 GMT" };
}
[Fact]
public void ToString_UseDifferentValues_MatchExpectation()
{
Assert.Equal("Sat, 31 Jul 2010 15:38:57 GMT",
HeaderUtilities.FormatDate(new DateTimeOffset(2010, 7, 31, 15, 38, 57, TimeSpan.Zero)));
Assert.Equal("Fri, 01 Jan 2010 01:01:01 GMT",
HeaderUtilities.FormatDate(new DateTimeOffset(2010, 1, 1, 1, 1, 1, TimeSpan.Zero)));
}
}
}

View File

@ -0,0 +1,533 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class EntityTagHeaderValueTest
{
[Fact]
public void Ctor_ETagNull_Throw()
{
Assert.Throws<ArgumentException>(() => new EntityTagHeaderValue(null));
// null and empty should be treated the same. So we also throw for empty strings.
Assert.Throws<ArgumentException>(() => new EntityTagHeaderValue(string.Empty));
}
[Fact]
public void Ctor_ETagInvalidFormat_ThrowFormatException()
{
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
AssertFormatException("tag");
AssertFormatException(" tag ");
AssertFormatException("\"tag\" invalid");
AssertFormatException("\"tag");
AssertFormatException("tag\"");
AssertFormatException("\"tag\"\"");
AssertFormatException("\"\"tag\"\"");
AssertFormatException("\"\"tag\"");
AssertFormatException("W/\"tag\""); // tag value must not contain 'W/'
}
[Fact]
public void Ctor_ETagValidFormat_SuccessfullyCreated()
{
var etag = new EntityTagHeaderValue("\"tag\"");
Assert.Equal("\"tag\"", etag.Tag);
Assert.False(etag.IsWeak, "IsWeak");
}
[Fact]
public void Ctor_ETagValidFormatAndIsWeak_SuccessfullyCreated()
{
var etag = new EntityTagHeaderValue("\"e tag\"", true);
Assert.Equal("\"e tag\"", etag.Tag);
Assert.True(etag.IsWeak, "IsWeak");
}
[Fact]
public void ToString_UseDifferentETags_AllSerializedCorrectly()
{
var etag = new EntityTagHeaderValue("\"e tag\"");
Assert.Equal("\"e tag\"", etag.ToString());
etag = new EntityTagHeaderValue("\"e tag\"", true);
Assert.Equal("W/\"e tag\"", etag.ToString());
etag = new EntityTagHeaderValue("\"\"", false);
Assert.Equal("\"\"", etag.ToString());
}
[Fact]
public void GetHashCode_UseSameAndDifferentETags_SameOrDifferentHashCodes()
{
var etag1 = new EntityTagHeaderValue("\"tag\"");
var etag2 = new EntityTagHeaderValue("\"TAG\"");
var etag3 = new EntityTagHeaderValue("\"tag\"", true);
var etag4 = new EntityTagHeaderValue("\"tag1\"");
var etag5 = new EntityTagHeaderValue("\"tag\"");
var etag6 = EntityTagHeaderValue.Any;
Assert.NotEqual(etag1.GetHashCode(), etag2.GetHashCode());
Assert.NotEqual(etag1.GetHashCode(), etag3.GetHashCode());
Assert.NotEqual(etag1.GetHashCode(), etag4.GetHashCode());
Assert.NotEqual(etag1.GetHashCode(), etag6.GetHashCode());
Assert.Equal(etag1.GetHashCode(), etag5.GetHashCode());
}
[Fact]
public void Equals_UseSameAndDifferentETags_EqualOrNotEqualNoExceptions()
{
var etag1 = new EntityTagHeaderValue("\"tag\"");
var etag2 = new EntityTagHeaderValue("\"TAG\"");
var etag3 = new EntityTagHeaderValue("\"tag\"", true);
var etag4 = new EntityTagHeaderValue("\"tag1\"");
var etag5 = new EntityTagHeaderValue("\"tag\"");
var etag6 = EntityTagHeaderValue.Any;
Assert.False(etag1.Equals(etag2), "Different casing.");
Assert.False(etag2.Equals(etag1), "Different casing.");
Assert.False(etag1.Equals(null), "tag vs. <null>.");
Assert.False(etag1.Equals(etag3), "strong vs. weak.");
Assert.False(etag3.Equals(etag1), "weak vs. strong.");
Assert.False(etag1.Equals(etag4), "tag vs. tag1.");
Assert.False(etag1.Equals(etag6), "tag vs. *.");
Assert.True(etag1.Equals(etag5), "tag vs. tag..");
}
[Fact]
public void Compare_WithNull_ReturnsFalse()
{
Assert.False(EntityTagHeaderValue.Any.Compare(null, useStrongComparison: true));
Assert.False(EntityTagHeaderValue.Any.Compare(null, useStrongComparison: false));
}
public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> NotEquivalentUnderStrongComparison
{
get
{
return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue>
{
{ new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"TAG\"") },
{ new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) },
{ new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) },
{ new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag1\"") },
{ new EntityTagHeaderValue("\"tag\""), EntityTagHeaderValue.Any },
};
}
}
[Theory]
[MemberData(nameof(NotEquivalentUnderStrongComparison))]
public void CompareUsingStrongComparison_NonEquivalentPairs_ReturnFalse(EntityTagHeaderValue left, EntityTagHeaderValue right)
{
Assert.False(left.Compare(right, useStrongComparison: true));
Assert.False(right.Compare(left, useStrongComparison: true));
}
public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> EquivalentUnderStrongComparison
{
get
{
return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue>
{
{ new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") },
};
}
}
[Theory]
[MemberData(nameof(EquivalentUnderStrongComparison))]
public void CompareUsingStrongComparison_EquivalentPairs_ReturnTrue(EntityTagHeaderValue left, EntityTagHeaderValue right)
{
Assert.True(left.Compare(right, useStrongComparison: true));
Assert.True(right.Compare(left, useStrongComparison: true));
}
public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> NotEquivalentUnderWeakComparison
{
get
{
return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue>
{
{ new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"TAG\"") },
{ new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag1\"") },
{ new EntityTagHeaderValue("\"tag\""), EntityTagHeaderValue.Any },
};
}
}
[Theory]
[MemberData(nameof(NotEquivalentUnderWeakComparison))]
public void CompareUsingWeakComparison_NonEquivalentPairs_ReturnFalse(EntityTagHeaderValue left, EntityTagHeaderValue right)
{
Assert.False(left.Compare(right, useStrongComparison: false));
Assert.False(right.Compare(left, useStrongComparison: false));
}
public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> EquivalentUnderWeakComparison
{
get
{
return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue>
{
{ new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") },
{ new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) },
{ new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) },
};
}
}
[Theory]
[MemberData(nameof(EquivalentUnderWeakComparison))]
public void CompareUsingWeakComparison_EquivalentPairs_ReturnTrue(EntityTagHeaderValue left, EntityTagHeaderValue right)
{
Assert.True(left.Compare(right, useStrongComparison: false));
Assert.True(right.Compare(left, useStrongComparison: false));
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\""));
CheckValidParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\""));
CheckValidParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\""));
CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\""));
CheckValidParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\""));
CheckValidParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true));
CheckValidParse("*", new EntityTagHeaderValue("*"));
}
[Fact]
public void Parse_SetOfInvalidValueStrings_Throws()
{
CheckInvalidParse(null);
CheckInvalidParse(string.Empty);
CheckInvalidParse(" ");
CheckInvalidParse(" !");
CheckInvalidParse("tag\" !");
CheckInvalidParse("!\"tag\"");
CheckInvalidParse("\"tag\",");
CheckInvalidParse("W");
CheckInvalidParse("W/");
CheckInvalidParse("W/\"");
CheckInvalidParse("\"tag\" \"tag2\"");
CheckInvalidParse("/\"tag\"");
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\""));
CheckValidTryParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\""));
CheckValidTryParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\""));
CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\""));
CheckValidTryParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\""));
CheckValidTryParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true));
CheckValidTryParse("*", new EntityTagHeaderValue("*"));
}
[Fact]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
{
CheckInvalidTryParse(null);
CheckInvalidTryParse(string.Empty);
CheckInvalidTryParse(" ");
CheckInvalidTryParse(" !");
CheckInvalidTryParse("tag\" !");
CheckInvalidTryParse("!\"tag\"");
CheckInvalidTryParse("\"tag\",");
CheckInvalidTryParse("\"tag\" \"tag2\"");
CheckInvalidTryParse("/\"tag\"");
}
[Fact]
public void ParseList_NullOrEmptyArray_ReturnsEmptyList()
{
var result = EntityTagHeaderValue.ParseList(null);
Assert.NotNull(result);
Assert.Equal(0, result.Count);
result = EntityTagHeaderValue.ParseList(new string[0]);
Assert.NotNull(result);
Assert.Equal(0, result.Count);
result = EntityTagHeaderValue.ParseList(new string[] { "" });
Assert.NotNull(result);
Assert.Equal(0, result.Count);
}
[Fact]
public void TryParseList_NullOrEmptyArray_ReturnsFalse()
{
IList<EntityTagHeaderValue> results = null;
Assert.False(EntityTagHeaderValue.TryParseList(null, out results));
Assert.False(EntityTagHeaderValue.TryParseList(new string[0], out results));
Assert.False(EntityTagHeaderValue.TryParseList(new string[] { "" }, out results));
}
[Fact]
public void ParseList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"\"tag\"",
"",
" \"tag\" ",
"\r\n \"tag\"\r\n ",
"\"tag会\"",
"\"tag\",\"tag\"",
"\"tag\", \"tag\"",
"W/\"tag\"",
};
IList<EntityTagHeaderValue> results = EntityTagHeaderValue.ParseList(inputs);
var expectedResults = new[]
{
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag会\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\"", true),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"\"tag\"",
"",
" \"tag\" ",
"\r\n \"tag\"\r\n ",
"\"tag会\"",
"\"tag\",\"tag\"",
"\"tag\", \"tag\"",
"W/\"tag\"",
};
IList<EntityTagHeaderValue> results = EntityTagHeaderValue.ParseStrictList(inputs);
var expectedResults = new[]
{
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag会\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\"", true),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"\"tag\"",
"",
" \"tag\" ",
"\r\n \"tag\"\r\n ",
"\"tag会\"",
"\"tag\",\"tag\"",
"\"tag\", \"tag\"",
"W/\"tag\"",
};
IList<EntityTagHeaderValue> results;
Assert.True(EntityTagHeaderValue.TryParseList(inputs, out results));
var expectedResults = new[]
{
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag会\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\"", true),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"\"tag\"",
"",
" \"tag\" ",
"\r\n \"tag\"\r\n ",
"\"tag会\"",
"\"tag\",\"tag\"",
"\"tag\", \"tag\"",
"W/\"tag\"",
};
IList<EntityTagHeaderValue> results;
Assert.True(EntityTagHeaderValue.TryParseStrictList(inputs, out results));
var expectedResults = new[]
{
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag会\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\"", true),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseList_WithSomeInvlaidValues_ExcludesInvalidValues()
{
var inputs = new[]
{
"",
"\"tag\", tag, \"tag\"",
"tag, \"tag\"",
"",
" \"tag ",
"\r\n tag\"\r\n ",
"\"tag会\"",
"\"tag\", \"tag\"",
"W/\"tag\"",
};
var results = EntityTagHeaderValue.ParseList(inputs);
var expectedResults = new[]
{
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag会\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\"", true),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseStrictList_WithSomeInvlaidValues_Throws()
{
var inputs = new[]
{
"",
"\"tag\", tag, \"tag\"",
"tag, \"tag\"",
"",
" \"tag ",
"\r\n tag\"\r\n ",
"\"tag会\"",
"\"tag\", \"tag\"",
"W/\"tag\"",
};
Assert.Throws<FormatException>(() => EntityTagHeaderValue.ParseStrictList(inputs));
}
[Fact]
public void TryParseList_WithSomeInvlaidValues_ExcludesInvalidValues()
{
var inputs = new[]
{
"",
"\"tag\", tag, \"tag\"",
"tag, \"tag\"",
"",
" \"tag ",
"\r\n tag\"\r\n ",
"\"tag会\"",
"\"tag\", \"tag\"",
"W/\"tag\"",
};
IList<EntityTagHeaderValue> results;
Assert.True(EntityTagHeaderValue.TryParseList(inputs, out results));
var expectedResults = new[]
{
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag会\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\""),
new EntityTagHeaderValue("\"tag\"", true),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse()
{
var inputs = new[]
{
"",
"\"tag\", tag, \"tag\"",
"tag, \"tag\"",
"",
" \"tag ",
"\r\n tag\"\r\n ",
"\"tag会\"",
"\"tag\", \"tag\"",
"W/\"tag\"",
};
IList<EntityTagHeaderValue> results;
Assert.False(EntityTagHeaderValue.TryParseStrictList(inputs, out results));
}
private void CheckValidParse(string input, EntityTagHeaderValue expectedResult)
{
var result = EntityTagHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckInvalidParse(string input)
{
Assert.Throws<FormatException>(() => EntityTagHeaderValue.Parse(input));
}
private void CheckValidTryParse(string input, EntityTagHeaderValue expectedResult)
{
EntityTagHeaderValue result = null;
Assert.True(EntityTagHeaderValue.TryParse(input, out result));
Assert.Equal(expectedResult, result);
}
private void CheckInvalidTryParse(string input)
{
EntityTagHeaderValue result = null;
Assert.False(EntityTagHeaderValue.TryParse(input, out result));
Assert.Null(result);
}
private static void AssertFormatException(string tag)
{
Assert.Throws<FormatException>(() => new EntityTagHeaderValue(tag));
}
}
}

View File

@ -0,0 +1,285 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class HeaderUtilitiesTest
{
private const string Rfc1123Format = "r";
[Theory]
[MemberData(nameof(TestValues))]
public void ReturnsSameResultAsRfc1123String(DateTimeOffset dateTime, bool quoted)
{
var formatted = dateTime.ToString(Rfc1123Format);
var expected = quoted ? $"\"{formatted}\"" : formatted;
var actual = HeaderUtilities.FormatDate(dateTime, quoted);
Assert.Equal(expected, actual);
}
public static TheoryData<DateTimeOffset, bool> TestValues
{
get
{
var data = new TheoryData<DateTimeOffset, bool>();
var date = new DateTimeOffset(new DateTime(2018, 1, 1, 1, 1, 1));
foreach (var quoted in new[] { true, false })
{
data.Add(date, quoted);
for (var i = 1; i < 60; i++)
{
data.Add(date.AddSeconds(i), quoted);
data.Add(date.AddMinutes(i), quoted);
}
for (var i = 1; i < DateTime.DaysInMonth(date.Year, date.Month); i++)
{
data.Add(date.AddDays(i), quoted);
}
for (var i = 1; i < 11; i++)
{
data.Add(date.AddMonths(i), quoted);
}
for (var i = 1; i < 5; i++)
{
data.Add(date.AddYears(i), quoted);
}
}
return data;
}
}
[Theory]
[InlineData("h=1", "h", 1)]
[InlineData("directive1=3, directive2=10", "directive1", 3)]
[InlineData("directive1 =45, directive2=80", "directive1", 45)]
[InlineData("directive1= 89 , directive2=22", "directive1", 89)]
[InlineData("directive1= 89 , directive2= 42", "directive2", 42)]
[InlineData("directive1= 89 , directive= 42", "directive", 42)]
[InlineData("directive1,,,,,directive2 = 42 ", "directive2", 42)]
[InlineData("directive1=;,directive2 = 42 ", "directive2", 42)]
[InlineData("directive1;;,;;,directive2 = 42 ", "directive2", 42)]
[InlineData("directive1=value;q=0.6,directive2 = 42 ", "directive2", 42)]
public void TryParseSeconds_Succeeds(string headerValues, string targetValue, int expectedValue)
{
TimeSpan? value;
Assert.True(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value));
Assert.Equal(TimeSpan.FromSeconds(expectedValue), value);
}
[Theory]
[InlineData("", "")]
[InlineData(null, null)]
[InlineData("h=", "h")]
[InlineData("directive1=, directive2=10", "directive1")]
[InlineData("directive1 , directive2=80", "directive1")]
[InlineData("h=10", "directive")]
[InlineData("directive1", "directive")]
[InlineData("directive1,,,,,,,", "directive")]
[InlineData("h=directive", "directive")]
[InlineData("directive1, directive2=80", "directive")]
[InlineData("directive1=;, directive2=10", "directive1")]
[InlineData("directive1;directive2=10", "directive2")]
public void TryParseSeconds_Fails(string headerValues, string targetValue)
{
TimeSpan? value;
Assert.False(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value));
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(1234567890)]
[InlineData(long.MaxValue)]
public void FormatNonNegativeInt64_MatchesToString(long value)
{
Assert.Equal(value.ToString(CultureInfo.InvariantCulture), HeaderUtilities.FormatNonNegativeInt64(value));
}
[Theory]
[InlineData(-1)]
[InlineData(-1234567890)]
[InlineData(long.MinValue)]
public void FormatNonNegativeInt64_Throws_ForNegativeValues(long value)
{
Assert.Throws<ArgumentOutOfRangeException>(() => HeaderUtilities.FormatNonNegativeInt64(value));
}
[Theory]
[InlineData("h", "h", true)]
[InlineData("h=", "h", true)]
[InlineData("h=1", "h", true)]
[InlineData("H", "h", true)]
[InlineData("H=", "h", true)]
[InlineData("H=1", "h", true)]
[InlineData("h", "H", true)]
[InlineData("h=", "H", true)]
[InlineData("h=1", "H", true)]
[InlineData("directive1, directive=10", "directive1", true)]
[InlineData("directive1=, directive=10", "directive1", true)]
[InlineData("directive1=3, directive=10", "directive1", true)]
[InlineData("directive1 , directive=80", "directive1", true)]
[InlineData(" directive1, directive=80", "directive1", true)]
[InlineData("directive1 =45, directive=80", "directive1", true)]
[InlineData("directive1= 89 , directive=22", "directive1", true)]
[InlineData("directive1, directive", "directive", true)]
[InlineData("directive1, directive=", "directive", true)]
[InlineData("directive1, directive=10", "directive", true)]
[InlineData("directive1=3, directive", "directive", true)]
[InlineData("directive1=3, directive=", "directive", true)]
[InlineData("directive1=3, directive=10", "directive", true)]
[InlineData("directive1= 89 , directive= 42", "directive", true)]
[InlineData("directive1= 89 , directive = 42", "directive", true)]
[InlineData("directive1,,,,,directive2 = 42 ", "directive2", true)]
[InlineData("directive1;;,;;,directive2 = 42 ", "directive2", true)]
[InlineData("directive1=;,directive2 = 42 ", "directive2", true)]
[InlineData("directive1=value;q=0.6,directive2 = 42 ", "directive2", true)]
[InlineData(null, null, false)]
[InlineData(null, "", false)]
[InlineData("", null, false)]
[InlineData("", "", false)]
[InlineData("h=10", "directive", false)]
[InlineData("directive1", "directive", false)]
[InlineData("directive1,,,,,,,", "directive", false)]
[InlineData("h=directive", "directive", false)]
[InlineData("directive1, directive2=80", "directive", false)]
[InlineData("directive1;, directive2=80", "directive", false)]
[InlineData("directive1=value;q=0.6;directive2 = 42 ", "directive2", false)]
public void ContainsCacheDirective_MatchesExactValue(string headerValues, string targetValue, bool contains)
{
Assert.Equal(contains, HeaderUtilities.ContainsCacheDirective(new StringValues(headerValues), targetValue));
}
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData("-1")]
[InlineData("a")]
[InlineData("1.1")]
[InlineData("9223372036854775808")] // long.MaxValue + 1
public void TryParseNonNegativeInt64_Fails(string valueString)
{
long value = 1;
Assert.False(HeaderUtilities.TryParseNonNegativeInt64(valueString, out value));
Assert.Equal(0, value);
}
[Theory]
[InlineData("0", 0)]
[InlineData("9223372036854775807", 9223372036854775807)] // long.MaxValue
public void TryParseNonNegativeInt64_Succeeds(string valueString, long expected)
{
long value = 1;
Assert.True(HeaderUtilities.TryParseNonNegativeInt64(valueString, out value));
Assert.Equal(expected, value);
}
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData("-1")]
[InlineData("a")]
[InlineData("1.1")]
[InlineData("1,000")]
[InlineData("2147483648")] // int.MaxValue + 1
public void TryParseNonNegativeInt32_Fails(string valueString)
{
int value = 1;
Assert.False(HeaderUtilities.TryParseNonNegativeInt32(valueString, out value));
Assert.Equal(0, value);
}
[Theory]
[InlineData("0", 0)]
[InlineData("2147483647", 2147483647)] // int.MaxValue
public void TryParseNonNegativeInt32_Succeeds(string valueString, long expected)
{
int value = 1;
Assert.True(HeaderUtilities.TryParseNonNegativeInt32(valueString, out value));
Assert.Equal(expected, value);
}
[Theory]
[InlineData("\"hello\"", "hello")]
[InlineData("\"hello", "\"hello")]
[InlineData("hello\"", "hello\"")]
[InlineData("\"\"hello\"\"", "\"hello\"")]
public void RemoveQuotes_BehaviorCheck(string input, string expected)
{
var actual = HeaderUtilities.RemoveQuotes(input);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData("\"hello\"", true)]
[InlineData("\"hello", false)]
[InlineData("hello\"", false)]
[InlineData("\"\"hello\"\"", true)]
public void IsQuoted_BehaviorCheck(string input, bool expected)
{
var actual = HeaderUtilities.IsQuoted(input);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData("value", "value")]
[InlineData("\"value\"", "value")]
[InlineData("\"hello\\\\\"", "hello\\")]
[InlineData("\"hello\\\"\"", "hello\"")]
[InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")]
[InlineData("\"quoted value\"", "quoted value")]
[InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")]
[InlineData("\"hello\\\"", "hello\\")]
public void UnescapeAsQuotedString_BehaviorCheck(string input, string expected)
{
var actual = HeaderUtilities.UnescapeAsQuotedString(input);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData("value", "\"value\"")]
[InlineData("23", "\"23\"")]
[InlineData(";;;", "\";;;\"")]
[InlineData("\"value\"", "\"\\\"value\\\"\"")]
[InlineData("unquoted \"value", "\"unquoted \\\"value\"")]
[InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")]
// We have to assume that the input needs to be quoted here
[InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")]
[InlineData("\t", "\"\t\"")]
public void SetAndEscapeValue_BehaviorCheck(string input, string expected)
{
var actual = HeaderUtilities.EscapeAsQuotedString(input);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData("\n")]
[InlineData("\b")]
[InlineData("\r")]
public void SetAndEscapeValue_ControlCharactersThrowFormatException(string input)
{
Assert.Throws<FormatException>(() => { var actual = HeaderUtilities.EscapeAsQuotedString(input); });
}
[Fact]
public void SetAndEscapeValue_ThrowsFormatExceptionOnDelCharacter()
{
Assert.Throws<FormatException>(() => { var actual = HeaderUtilities.EscapeAsQuotedString($"{(char)0x7F}"); });
}
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class MediaTypeHeaderValueComparerTests
{
public static IEnumerable<object[]> SortValues
{
get
{
yield return new object[] {
new string[]
{
"application/*",
"text/plain",
"text/*+json;q=0.8",
"text/plain;q=1.0",
"text/plain",
"text/*+json;q=0.6",
"text/plain;q=0",
"*/*;q=0.8",
"*/*;q=1",
"text/*;q=1",
"text/plain;q=0.8",
"text/*;q=0.8",
"text/*;q=0.6",
"text/*+json;q=0.4",
"text/*;q=1.0",
"*/*;q=0.4",
"text/plain;q=0.6",
"text/xml",
},
new string[]
{
"text/plain",
"text/plain;q=1.0",
"text/plain",
"text/xml",
"application/*",
"text/*;q=1",
"text/*;q=1.0",
"*/*;q=1",
"text/plain;q=0.8",
"text/*+json;q=0.8",
"text/*;q=0.8",
"*/*;q=0.8",
"text/plain;q=0.6",
"text/*+json;q=0.6",
"text/*;q=0.6",
"text/*+json;q=0.4",
"*/*;q=0.4",
"text/plain;q=0",
}
};
}
}
[Theory]
[MemberData(nameof(SortValues))]
public void SortMediaTypeHeaderValuesByQFactor_SortsCorrectly(IEnumerable<string> unsorted, IEnumerable<string> expectedSorted)
{
var unsortedValues = MediaTypeHeaderValue.ParseList(unsorted.ToList());
var expectedSortedValues = MediaTypeHeaderValue.ParseList(expectedSorted.ToList());
var actualSorted = unsortedValues.OrderByDescending(m => m, MediaTypeHeaderValueComparer.QualityComparer).ToList();
Assert.Equal(expectedSortedValues, actualSorted);
}
}
}

View File

@ -0,0 +1,847 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class MediaTypeHeaderValueTest
{
[Fact]
public void Ctor_MediaTypeNull_Throw()
{
Assert.Throws<ArgumentException>(() => new MediaTypeHeaderValue(null));
// null and empty should be treated the same. So we also throw for empty strings.
Assert.Throws<ArgumentException>(() => new MediaTypeHeaderValue(string.Empty));
}
[Fact]
public void Ctor_MediaTypeInvalidFormat_ThrowFormatException()
{
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
AssertFormatException(" text/plain ");
AssertFormatException("text / plain");
AssertFormatException("text/ plain");
AssertFormatException("text /plain");
AssertFormatException("text/plain ");
AssertFormatException(" text/plain");
AssertFormatException("te xt/plain");
AssertFormatException("te=xt/plain");
AssertFormatException("teäxt/plain");
AssertFormatException("text/pläin");
AssertFormatException("text");
AssertFormatException("\"text/plain\"");
AssertFormatException("text/plain; charset=utf-8; ");
AssertFormatException("text/plain;");
AssertFormatException("text/plain;charset=utf-8"); // ctor takes only media-type name, no parameters
}
public static TheoryData<string, string, string> MediaTypesWithSuffixes =>
new TheoryData<string, string, string>
{
// See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec
{ "application/json", "json", null },
{ "application/json+", "json", "" },
{ "application/+json", "", "json" },
{ "application/entitytype+json", "entitytype", "json" },
{ "applica+tion/entitytype+json", "entitytype", "json" },
};
[Theory]
[MemberData(nameof(MediaTypesWithSuffixes))]
public void Ctor_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix)
{
var result = new MediaTypeHeaderValue(mediaType);
Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix?
Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix);
}
public static TheoryData<string, string, string> MediaTypesWithSuffixesAndSpaces =>
new TheoryData<string, string, string>
{
// See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec
{ " application / json+xml", "json", "xml" },
{ " application / vnd.com-pany.some+entity!.v2+js.#$&^_n ; q=\"0.3+1\"", "vnd.com-pany.some+entity!.v2", "js.#$&^_n"},
{ " application/ +json", "", "json" },
{ " application/ entitytype+json ", "entitytype", "json" },
{ " applica+tion/ entitytype+json ", "entitytype", "json" }
};
[Theory]
[MemberData(nameof(MediaTypesWithSuffixesAndSpaces))]
public void Parse_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix)
{
var result = MediaTypeHeaderValue.Parse(mediaType);
Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix?
Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix);
}
[Theory]
[InlineData("*/*", true)]
[InlineData("text/*", true)]
[InlineData("text/*+suffix", true)]
[InlineData("text/*+", true)]
[InlineData("text/*+*", true)]
[InlineData("text/json+suffix", false)]
[InlineData("*/json+*", false)]
public void MatchesAllSubTypesWithoutSuffix_ReturnsExpectedResult(string value, bool expectedReturnValue)
{
// Arrange
var mediaType = new MediaTypeHeaderValue(value);
// Act
var result = mediaType.MatchesAllSubTypesWithoutSuffix;
// Assert
Assert.Equal(expectedReturnValue, result);
}
[Fact]
public void Ctor_MediaTypeValidFormat_SuccessfullyCreated()
{
var mediaType = new MediaTypeHeaderValue("text/plain");
Assert.Equal("text/plain", mediaType.MediaType);
Assert.Equal(0, mediaType.Parameters.Count);
Assert.Null(mediaType.Charset.Value);
}
[Fact]
public void Ctor_AddNameAndQuality_QualityParameterAdded()
{
var mediaType = new MediaTypeHeaderValue("application/xml", 0.08);
Assert.Equal(0.08, mediaType.Quality);
Assert.Equal("application/xml", mediaType.MediaType);
Assert.Equal(1, mediaType.Parameters.Count);
}
[Fact]
public void Parameters_AddNull_Throw()
{
var mediaType = new MediaTypeHeaderValue("text/plain");
Assert.Throws<ArgumentNullException>(() => mediaType.Parameters.Add(null));
}
[Fact]
public void Copy_SimpleMediaType_Copied()
{
var mediaType0 = new MediaTypeHeaderValue("text/plain");
var mediaType1 = mediaType0.Copy();
Assert.NotSame(mediaType0, mediaType1);
Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
}
[Fact]
public void CopyAsReadOnly_SimpleMediaType_CopiedAndReadOnly()
{
var mediaType0 = new MediaTypeHeaderValue("text/plain");
var mediaType1 = mediaType0.CopyAsReadOnly();
Assert.NotSame(mediaType0, mediaType1);
Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
Assert.False(mediaType0.IsReadOnly);
Assert.True(mediaType1.IsReadOnly);
Assert.Throws<InvalidOperationException>(() => { mediaType1.MediaType = "some/value"; });
}
[Fact]
public void Copy_WithParameters_Copied()
{
var mediaType0 = new MediaTypeHeaderValue("text/plain");
mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value"));
var mediaType1 = mediaType0.Copy();
Assert.NotSame(mediaType0, mediaType1);
Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
var pair0 = mediaType0.Parameters.First();
var pair1 = mediaType1.Parameters.First();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name.Value, pair1.Name.Value);
Assert.Same(pair0.Value.Value, pair1.Value.Value);
}
[Fact]
public void CopyAsReadOnly_WithParameters_CopiedAndReadOnly()
{
var mediaType0 = new MediaTypeHeaderValue("text/plain");
mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value"));
var mediaType1 = mediaType0.CopyAsReadOnly();
Assert.NotSame(mediaType0, mediaType1);
Assert.False(mediaType0.IsReadOnly);
Assert.True(mediaType1.IsReadOnly);
Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
Assert.False(mediaType0.Parameters.IsReadOnly);
Assert.True(mediaType1.Parameters.IsReadOnly);
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
Assert.Throws<NotSupportedException>(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name")));
Assert.Throws<NotSupportedException>(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name")));
Assert.Throws<NotSupportedException>(() => mediaType1.Parameters.Clear());
var pair0 = mediaType0.Parameters.First();
var pair1 = mediaType1.Parameters.First();
Assert.NotSame(pair0, pair1);
Assert.False(pair0.IsReadOnly);
Assert.True(pair1.IsReadOnly);
Assert.Same(pair0.Name.Value, pair1.Name.Value);
Assert.Same(pair0.Value.Value, pair1.Value.Value);
}
[Fact]
public void CopyFromReadOnly_WithParameters_CopiedAsNonReadOnly()
{
var mediaType0 = new MediaTypeHeaderValue("text/plain");
mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value"));
var mediaType1 = mediaType0.CopyAsReadOnly();
var mediaType2 = mediaType1.Copy();
Assert.NotSame(mediaType2, mediaType1);
Assert.Same(mediaType2.MediaType.Value, mediaType1.MediaType.Value);
Assert.True(mediaType1.IsReadOnly);
Assert.False(mediaType2.IsReadOnly);
Assert.NotSame(mediaType2.Parameters, mediaType1.Parameters);
Assert.Equal(mediaType2.Parameters.Count, mediaType1.Parameters.Count);
var pair2 = mediaType2.Parameters.First();
var pair1 = mediaType1.Parameters.First();
Assert.NotSame(pair2, pair1);
Assert.True(pair1.IsReadOnly);
Assert.False(pair2.IsReadOnly);
Assert.Same(pair2.Name.Value, pair1.Name.Value);
Assert.Same(pair2.Value.Value, pair1.Value.Value);
}
[Fact]
public void MediaType_SetAndGetMediaType_MatchExpectations()
{
var mediaType = new MediaTypeHeaderValue("text/plain");
Assert.Equal("text/plain", mediaType.MediaType);
mediaType.MediaType = "application/xml";
Assert.Equal("application/xml", mediaType.MediaType);
}
[Fact]
public void Charset_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded()
{
var mediaType = new MediaTypeHeaderValue("text/plain");
mediaType.Charset = "mycharset";
Assert.Equal("mycharset", mediaType.Charset);
Assert.Equal(1, mediaType.Parameters.Count);
Assert.Equal("charset", mediaType.Parameters.First().Name);
mediaType.Charset = null;
Assert.Null(mediaType.Charset.Value);
Assert.Equal(0, mediaType.Parameters.Count);
mediaType.Charset = null; // It's OK to set it again to null; no exception.
}
[Fact]
public void Charset_AddCharsetParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var mediaType = new MediaTypeHeaderValue("text/plain");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var charset = new NameValueHeaderValue("CHARSET", "old_charset");
mediaType.Parameters.Add(charset);
Assert.Equal(1, mediaType.Parameters.Count);
Assert.Equal("CHARSET", mediaType.Parameters.First().Name);
mediaType.Charset = "new_charset";
Assert.Equal("new_charset", mediaType.Charset);
Assert.Equal(1, mediaType.Parameters.Count);
Assert.Equal("CHARSET", mediaType.Parameters.First().Name);
mediaType.Parameters.Remove(charset);
Assert.Null(mediaType.Charset.Value);
}
[Fact]
public void Quality_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded()
{
var mediaType = new MediaTypeHeaderValue("text/plain");
mediaType.Quality = 0.563156454;
Assert.Equal(0.563, mediaType.Quality);
Assert.Equal(1, mediaType.Parameters.Count);
Assert.Equal("q", mediaType.Parameters.First().Name);
Assert.Equal("0.563", mediaType.Parameters.First().Value);
mediaType.Quality = null;
Assert.Null(mediaType.Quality);
Assert.Equal(0, mediaType.Parameters.Count);
mediaType.Quality = null; // It's OK to set it again to null; no exception.
}
[Fact]
public void Quality_AddQualityParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var mediaType = new MediaTypeHeaderValue("text/plain");
var quality = new NameValueHeaderValue("q", "0.132");
mediaType.Parameters.Add(quality);
Assert.Equal(1, mediaType.Parameters.Count);
Assert.Equal("q", mediaType.Parameters.First().Name);
Assert.Equal(0.132, mediaType.Quality);
mediaType.Quality = 0.9;
Assert.Equal(0.9, mediaType.Quality);
Assert.Equal(1, mediaType.Parameters.Count);
Assert.Equal("q", mediaType.Parameters.First().Name);
mediaType.Parameters.Remove(quality);
Assert.Null(mediaType.Quality);
}
[Fact]
public void Quality_AddQualityParameterUpperCase_CaseInsensitiveComparison()
{
var mediaType = new MediaTypeHeaderValue("text/plain");
var quality = new NameValueHeaderValue("Q", "0.132");
mediaType.Parameters.Add(quality);
Assert.Equal(1, mediaType.Parameters.Count);
Assert.Equal("Q", mediaType.Parameters.First().Name);
Assert.Equal(0.132, mediaType.Quality);
}
[Fact]
public void Quality_LessThanZero_Throw()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new MediaTypeHeaderValue("application/xml", -0.01));
}
[Fact]
public void Quality_GreaterThanOne_Throw()
{
var mediaType = new MediaTypeHeaderValue("application/xml");
Assert.Throws<ArgumentOutOfRangeException>(() => mediaType.Quality = 1.01);
}
[Fact]
public void ToString_UseDifferentMediaTypes_AllSerializedCorrectly()
{
var mediaType = new MediaTypeHeaderValue("text/plain");
Assert.Equal("text/plain", mediaType.ToString());
mediaType.Charset = "utf-8";
Assert.Equal("text/plain; charset=utf-8", mediaType.ToString());
mediaType.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\""));
Assert.Equal("text/plain; charset=utf-8; custom=\"custom value\"", mediaType.ToString());
mediaType.Charset = null;
Assert.Equal("text/plain; custom=\"custom value\"", mediaType.ToString());
}
[Fact]
public void GetHashCode_UseMediaTypeWithAndWithoutParameters_SameOrDifferentHashCodes()
{
var mediaType1 = new MediaTypeHeaderValue("text/plain");
var mediaType2 = new MediaTypeHeaderValue("text/plain");
mediaType2.Charset = "utf-8";
var mediaType3 = new MediaTypeHeaderValue("text/plain");
mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value"));
var mediaType4 = new MediaTypeHeaderValue("TEXT/plain");
var mediaType5 = new MediaTypeHeaderValue("TEXT/plain");
mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8"));
Assert.NotEqual(mediaType1.GetHashCode(), mediaType2.GetHashCode());
Assert.NotEqual(mediaType1.GetHashCode(), mediaType3.GetHashCode());
Assert.NotEqual(mediaType2.GetHashCode(), mediaType3.GetHashCode());
Assert.Equal(mediaType1.GetHashCode(), mediaType4.GetHashCode());
Assert.Equal(mediaType2.GetHashCode(), mediaType5.GetHashCode());
}
[Fact]
public void Equals_UseMediaTypeWithAndWithoutParameters_EqualOrNotEqualNoExceptions()
{
var mediaType1 = new MediaTypeHeaderValue("text/plain");
var mediaType2 = new MediaTypeHeaderValue("text/plain");
mediaType2.Charset = "utf-8";
var mediaType3 = new MediaTypeHeaderValue("text/plain");
mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value"));
var mediaType4 = new MediaTypeHeaderValue("TEXT/plain");
var mediaType5 = new MediaTypeHeaderValue("TEXT/plain");
mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8"));
var mediaType6 = new MediaTypeHeaderValue("TEXT/plain");
mediaType6.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8"));
mediaType6.Parameters.Add(new NameValueHeaderValue("custom", "value"));
var mediaType7 = new MediaTypeHeaderValue("text/other");
Assert.False(mediaType1.Equals(mediaType2), "No params vs. charset.");
Assert.False(mediaType2.Equals(mediaType1), "charset vs. no params.");
Assert.False(mediaType1.Equals(null), "No params vs. <null>.");
Assert.False(mediaType1.Equals(mediaType3), "No params vs. custom param.");
Assert.False(mediaType2.Equals(mediaType3), "charset vs. custom param.");
Assert.True(mediaType1.Equals(mediaType4), "Different casing.");
Assert.True(mediaType2.Equals(mediaType5), "Different casing in charset.");
Assert.False(mediaType5.Equals(mediaType6), "charset vs. custom param.");
Assert.False(mediaType1.Equals(mediaType7), "text/plain vs. text/other.");
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidParse("\r\n text/plain ", new MediaTypeHeaderValue("text/plain"));
CheckValidParse("text/plain", new MediaTypeHeaderValue("text/plain"));
CheckValidParse("\r\n text / plain ; charset = utf-8 ", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" });
CheckValidParse(" text/plain;charset=utf-8", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" });
CheckValidParse("text/plain; charset=iso-8859-1", new MediaTypeHeaderValue("text/plain") { Charset = "iso-8859-1" });
var expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" };
expected.Parameters.Add(new NameValueHeaderValue("custom", "value"));
CheckValidParse(" text/plain; custom=value;charset=utf-8", expected);
expected = new MediaTypeHeaderValue("text/plain");
expected.Parameters.Add(new NameValueHeaderValue("custom"));
CheckValidParse(" text/plain; custom", expected);
expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" };
expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\""));
CheckValidParse("text / plain ; custom =\r\n \"x\" ; charset = utf-8 ", expected);
expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" };
expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\""));
CheckValidParse("text/plain;custom=\"x\";charset=utf-8", expected);
expected = new MediaTypeHeaderValue("text/plain");
CheckValidParse("text/plain;", expected);
expected = new MediaTypeHeaderValue("text/plain");
expected.Parameters.Add(new NameValueHeaderValue("name", ""));
CheckValidParse("text/plain;name=", expected);
expected = new MediaTypeHeaderValue("text/plain");
expected.Parameters.Add(new NameValueHeaderValue("name", "value"));
CheckValidParse("text/plain;name=value;", expected);
expected = new MediaTypeHeaderValue("text/plain");
expected.Charset = "iso-8859-1";
expected.Quality = 1.0;
CheckValidParse("text/plain; charset=iso-8859-1; q=1.0", expected);
expected = new MediaTypeHeaderValue("*/xml");
expected.Charset = "utf-8";
expected.Quality = 0.5;
CheckValidParse("\r\n */xml; charset=utf-8; q=0.5", expected);
expected = new MediaTypeHeaderValue("*/*");
CheckValidParse("*/*", expected);
expected = new MediaTypeHeaderValue("text/*");
expected.Charset = "utf-8";
expected.Parameters.Add(new NameValueHeaderValue("foo", "bar"));
CheckValidParse("text/*; charset=utf-8; foo=bar", expected);
expected = new MediaTypeHeaderValue("text/plain");
expected.Charset = "utf-8";
expected.Quality = 0;
expected.Parameters.Add(new NameValueHeaderValue("foo", "bar"));
CheckValidParse("text/plain; charset=utf-8; foo=bar; q=0.0", expected);
}
[Fact]
public void Parse_SetOfInvalidValueStrings_Throws()
{
CheckInvalidParse("");
CheckInvalidParse(" ");
CheckInvalidParse(null);
CheckInvalidParse("text/plain会");
CheckInvalidParse("text/plain ,");
CheckInvalidParse("text/plain,");
CheckInvalidParse("text/plain; charset=utf-8 ,");
CheckInvalidParse("text/plain; charset=utf-8,");
CheckInvalidParse("textplain");
CheckInvalidParse("text/");
CheckInvalidParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,");
CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5");
CheckInvalidParse(" , */xml; charset=utf-8; q=0.5 ");
CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0 , ");
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
var expected = new MediaTypeHeaderValue("text/plain");
CheckValidTryParse("\r\n text/plain ", expected);
CheckValidTryParse("text/plain", expected);
// We don't have to test all possible input strings, since most of the pieces are handled by other parsers.
// The purpose of this test is to verify that these other parsers are combined correctly to build a
// media-type parser.
expected.Charset = "utf-8";
CheckValidTryParse("\r\n text / plain ; charset = utf-8 ", expected);
CheckValidTryParse(" text/plain;charset=utf-8", expected);
var value1 = new MediaTypeHeaderValue("text/plain");
value1.Charset = "iso-8859-1";
value1.Quality = 1.0;
CheckValidTryParse("text/plain; charset=iso-8859-1; q=1.0", value1);
var value2 = new MediaTypeHeaderValue("*/xml");
value2.Charset = "utf-8";
value2.Quality = 0.5;
CheckValidTryParse("\r\n */xml; charset=utf-8; q=0.5", value2);
}
[Fact]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
{
CheckInvalidTryParse("");
CheckInvalidTryParse(" ");
CheckInvalidTryParse(null);
CheckInvalidTryParse("text/plain会");
CheckInvalidTryParse("text/plain ,");
CheckInvalidTryParse("text/plain,");
CheckInvalidTryParse("text/plain; charset=utf-8 ,");
CheckInvalidTryParse("text/plain; charset=utf-8,");
CheckInvalidTryParse("textplain");
CheckInvalidTryParse("text/");
CheckInvalidTryParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,");
CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5");
CheckInvalidTryParse(" , */xml; charset=utf-8; q=0.5 ");
CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0 , ");
}
[Fact]
public void ParseList_NullOrEmptyArray_ReturnsEmptyList()
{
var results = MediaTypeHeaderValue.ParseList(null);
Assert.NotNull(results);
Assert.Equal(0, results.Count);
results = MediaTypeHeaderValue.ParseList(new string[0]);
Assert.NotNull(results);
Assert.Equal(0, results.Count);
results = MediaTypeHeaderValue.ParseList(new string[] { "" });
Assert.NotNull(results);
Assert.Equal(0, results.Count);
}
[Fact]
public void TryParseList_NullOrEmptyArray_ReturnsFalse()
{
IList<MediaTypeHeaderValue> results;
Assert.False(MediaTypeHeaderValue.TryParseList(null, out results));
Assert.False(MediaTypeHeaderValue.TryParseList(new string[0], out results));
Assert.False(MediaTypeHeaderValue.TryParseList(new string[] { "" }, out results));
}
[Fact]
public void ParseList_SetOfValidValueStrings_ReturnsValues()
{
var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" };
var results = MediaTypeHeaderValue.ParseList(inputs);
var expectedResults = new[]
{
new MediaTypeHeaderValue("text/html"),
new MediaTypeHeaderValue("application/xhtml+xml"),
new MediaTypeHeaderValue("application/xml", 0.9),
new MediaTypeHeaderValue("image/webp"),
new MediaTypeHeaderValue("*/*", 0.8),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseStrictList_SetOfValidValueStrings_ReturnsValues()
{
var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" };
var results = MediaTypeHeaderValue.ParseStrictList(inputs);
var expectedResults = new[]
{
new MediaTypeHeaderValue("text/html"),
new MediaTypeHeaderValue("application/xhtml+xml"),
new MediaTypeHeaderValue("application/xml", 0.9),
new MediaTypeHeaderValue("image/webp"),
new MediaTypeHeaderValue("*/*", 0.8),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseList_SetOfValidValueStrings_ReturnsTrue()
{
var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" };
IList<MediaTypeHeaderValue> results;
Assert.True(MediaTypeHeaderValue.TryParseList(inputs, out results));
var expectedResults = new[]
{
new MediaTypeHeaderValue("text/html"),
new MediaTypeHeaderValue("application/xhtml+xml"),
new MediaTypeHeaderValue("application/xml", 0.9),
new MediaTypeHeaderValue("image/webp"),
new MediaTypeHeaderValue("*/*", 0.8),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseStrictList_SetOfValidValueStrings_ReturnsTrue()
{
var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" };
IList<MediaTypeHeaderValue> results;
Assert.True(MediaTypeHeaderValue.TryParseStrictList(inputs, out results));
var expectedResults = new[]
{
new MediaTypeHeaderValue("text/html"),
new MediaTypeHeaderValue("application/xhtml+xml"),
new MediaTypeHeaderValue("application/xml", 0.9),
new MediaTypeHeaderValue("image/webp"),
new MediaTypeHeaderValue("*/*", 0.8),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseList_WithSomeInvlaidValues_IgnoresInvalidValues()
{
var inputs = new[]
{
"text/html,application/xhtml+xml, ignore-this, ignore/this",
"application/xml;q=0.9,image/webp,*/*;q=0.8"
};
var results = MediaTypeHeaderValue.ParseList(inputs);
var expectedResults = new[]
{
new MediaTypeHeaderValue("text/html"),
new MediaTypeHeaderValue("application/xhtml+xml"),
new MediaTypeHeaderValue("ignore/this"),
new MediaTypeHeaderValue("application/xml", 0.9),
new MediaTypeHeaderValue("image/webp"),
new MediaTypeHeaderValue("*/*", 0.8),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseStrictList_WithSomeInvlaidValues_Throws()
{
var inputs = new[]
{
"text/html,application/xhtml+xml, ignore-this, ignore/this",
"application/xml;q=0.9,image/webp,*/*;q=0.8"
};
Assert.Throws<FormatException>(() => MediaTypeHeaderValue.ParseStrictList(inputs));
}
[Fact]
public void TryParseList_WithSomeInvlaidValues_IgnoresInvalidValues()
{
var inputs = new[]
{
"text/html,application/xhtml+xml, ignore-this, ignore/this",
"application/xml;q=0.9,image/webp,*/*;q=0.8",
"application/xml;q=0 4"
};
IList<MediaTypeHeaderValue> results;
Assert.True(MediaTypeHeaderValue.TryParseList(inputs, out results));
var expectedResults = new[]
{
new MediaTypeHeaderValue("text/html"),
new MediaTypeHeaderValue("application/xhtml+xml"),
new MediaTypeHeaderValue("ignore/this"),
new MediaTypeHeaderValue("application/xml", 0.9),
new MediaTypeHeaderValue("image/webp"),
new MediaTypeHeaderValue("*/*", 0.8),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse()
{
var inputs = new[]
{
"text/html,application/xhtml+xml, ignore-this, ignore/this",
"application/xml;q=0.9,image/webp,*/*;q=0.8",
"application/xml;q=0 4"
};
IList<MediaTypeHeaderValue> results;
Assert.False(MediaTypeHeaderValue.TryParseStrictList(inputs, out results));
}
[Theory]
[InlineData("*/*;", "*/*")]
[InlineData("text/*", "text/*")]
[InlineData("text/*;", "*/*")]
[InlineData("text/plain;", "text/plain")]
[InlineData("text/plain", "text/*")]
[InlineData("text/plain;", "*/*")]
[InlineData("*/*;missingparam=4", "*/*")]
[InlineData("text/*;missingparam=4;", "*/*;")]
[InlineData("text/plain;missingparam=4", "*/*;")]
[InlineData("text/plain;missingparam=4", "text/*")]
[InlineData("text/plain;charset=utf-8", "text/plain;charset=utf-8")]
[InlineData("text/plain;version=v1", "Text/plain;Version=v1")]
[InlineData("text/plain;version=v1", "tExT/plain;version=V1")]
[InlineData("text/plain;version=v1", "TEXT/PLAIN;VERSION=V1")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;foo=bar;q=0.0;charset=utf-8")] // different order of parameters
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;charset=utf-8;foo=bar;q=0.0")]
[InlineData("application/json;v=2", "application/json;*")]
[InlineData("application/json;v=2;charset=utf-8", "application/json;v=2;*")]
public void IsSubsetOf_PositiveCases(string mediaType1, string mediaType2)
{
// Arrange
var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1);
var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2);
// Act
var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2);
// Assert
Assert.True(isSubset);
}
[Theory]
[InlineData("application/html", "text/*")]
[InlineData("application/json", "application/html")]
[InlineData("text/plain;version=v1", "text/plain;version=")]
[InlineData("*/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")]
[InlineData("*/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;missingparam=4;")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;missingparam=4;")]
public void IsSubsetOf_NegativeCases(string mediaType1, string mediaType2)
{
// Arrange
var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1);
var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2);
// Act
var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2);
// Assert
Assert.False(isSubset);
}
[Theory]
[InlineData("application/entity+json", "application/entity+json")]
[InlineData("application/*+json", "application/entity+json")]
[InlineData("application/*+json", "application/*+json")]
[InlineData("application/*", "application/*+JSON")]
[InlineData("application/vnd.github+json", "application/vnd.github+json")]
[InlineData("application/*", "application/entity+JSON")]
[InlineData("*/*", "application/entity+json")]
public void IsSubsetOfWithSuffixes_PositiveCases(string set, string subset)
{
// Arrange
var setMediaType = MediaTypeHeaderValue.Parse(set);
var subSetMediaType = MediaTypeHeaderValue.Parse(subset);
// Act
var result = subSetMediaType.IsSubsetOf(setMediaType);
// Assert
Assert.True(result);
}
[Theory]
[InlineData("application/entity+json", "application/entity+txt")]
[InlineData("application/entity+json", "application/entity.v2+json")]
[InlineData("application/*+json", "application/entity+txt")]
[InlineData("application/*+*", "application/json")]
[InlineData("application/entity+*", "application/entity+json")] // We don't allow suffixes to be wildcards
[InlineData("application/*+*", "application/entity+json")] // We don't allow suffixes to be wildcards
public void IsSubSetOfWithSuffixes_NegativeCases(string set, string subset)
{
// Arrange
var setMediaType = MediaTypeHeaderValue.Parse(set);
var subSetMediaType = MediaTypeHeaderValue.Parse(subset);
// Act
var result = subSetMediaType.IsSubsetOf(setMediaType);
// Assert
Assert.False(result);
}
public static TheoryData<string, List<StringSegment>> MediaTypesWithFacets =>
new TheoryData<string, List<StringSegment>>
{
{ "application/vdn.github",
new List<StringSegment>(){ "vdn", "github" } },
{ "application/vdn.github+json",
new List<StringSegment>(){ "vdn", "github" } },
{ "application/vdn.github.v3+json",
new List<StringSegment>(){ "vdn", "github", "v3" } },
{ "application/vdn.github.+json",
new List<StringSegment>(){ "vdn", "github", "" } },
};
[Theory]
[MemberData(nameof(MediaTypesWithFacets))]
public void Facets_TestPositiveCases(string input, List<StringSegment> expected)
{
// Arrange
var mediaType = MediaTypeHeaderValue.Parse(input);
// Act
var result = mediaType.Facets;
// Assert
Assert.Equal(expected, result);
}
private void CheckValidParse(string input, MediaTypeHeaderValue expectedResult)
{
var result = MediaTypeHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckInvalidParse(string input)
{
Assert.Throws<FormatException>(() => MediaTypeHeaderValue.Parse(input));
}
private void CheckValidTryParse(string input, MediaTypeHeaderValue expectedResult)
{
MediaTypeHeaderValue result = null;
Assert.True(MediaTypeHeaderValue.TryParse(input, out result));
Assert.Equal(expectedResult, result);
}
private void CheckInvalidTryParse(string input)
{
MediaTypeHeaderValue result = null;
Assert.False(MediaTypeHeaderValue.TryParse(input, out result));
Assert.Null(result);
}
private static void AssertFormatException(string mediaType)
{
Assert.Throws<FormatException>(() => new MediaTypeHeaderValue(mediaType));
}
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Net.Http.Headers" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,699 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class NameValueHeaderValueTest
{
[Fact]
public void Ctor_NameNull_Throw()
{
Assert.Throws<ArgumentException>(() => new NameValueHeaderValue(null));
// null and empty should be treated the same. So we also throw for empty strings.
Assert.Throws<ArgumentException>(() => new NameValueHeaderValue(string.Empty));
}
[Fact]
public void Ctor_NameInvalidFormat_ThrowFormatException()
{
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
AssertFormatException(" text ", null);
AssertFormatException("text ", null);
AssertFormatException(" text", null);
AssertFormatException("te xt", null);
AssertFormatException("te=xt", null); // The ctor takes a name which must not contain '='.
AssertFormatException("teäxt", null);
}
[Fact]
public void Ctor_NameValidFormat_SuccessfullyCreated()
{
var nameValue = new NameValueHeaderValue("text", null);
Assert.Equal("text", nameValue.Name);
}
[Fact]
public void Ctor_ValueInvalidFormat_ThrowFormatException()
{
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
AssertFormatException("text", " token ");
AssertFormatException("text", "token ");
AssertFormatException("text", " token");
AssertFormatException("text", "token string");
AssertFormatException("text", "\"quoted string with \" quotes\"");
AssertFormatException("text", "\"quoted string with \"two\" quotes\"");
}
[Fact]
public void Ctor_ValueValidFormat_SuccessfullyCreated()
{
CheckValue(null);
CheckValue(string.Empty);
CheckValue("token_string");
CheckValue("\"quoted string\"");
CheckValue("\"quoted string with quoted \\\" quote-pair\"");
}
[Fact]
public void Copy_NameOnly_SuccesfullyCopied()
{
var pair0 = new NameValueHeaderValue("name");
var pair1 = pair0.Copy();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name.Value, pair1.Name.Value);
Assert.Null(pair0.Value.Value);
Assert.Null(pair1.Value.Value);
// Change one value and verify the other is unchanged.
pair0.Value = "othervalue";
Assert.Equal("othervalue", pair0.Value);
Assert.Null(pair1.Value.Value);
}
[Fact]
public void CopyAsReadOnly_NameOnly_CopiedAndReadOnly()
{
var pair0 = new NameValueHeaderValue("name");
var pair1 = pair0.CopyAsReadOnly();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name.Value, pair1.Name.Value);
Assert.Null(pair0.Value.Value);
Assert.Null(pair1.Value.Value);
Assert.False(pair0.IsReadOnly);
Assert.True(pair1.IsReadOnly);
// Change one value and verify the other is unchanged.
pair0.Value = "othervalue";
Assert.Equal("othervalue", pair0.Value);
Assert.Null(pair1.Value.Value);
Assert.Throws<InvalidOperationException>(() => { pair1.Value = "othervalue"; });
}
[Fact]
public void Copy_NameAndValue_SuccesfullyCopied()
{
var pair0 = new NameValueHeaderValue("name", "value");
var pair1 = pair0.Copy();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name.Value, pair1.Name.Value);
Assert.Same(pair0.Value.Value, pair1.Value.Value);
// Change one value and verify the other is unchanged.
pair0.Value = "othervalue";
Assert.Equal("othervalue", pair0.Value);
Assert.Equal("value", pair1.Value);
}
[Fact]
public void CopyAsReadOnly_NameAndValue_CopiedAndReadOnly()
{
var pair0 = new NameValueHeaderValue("name", "value");
var pair1 = pair0.CopyAsReadOnly();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name.Value, pair1.Name.Value);
Assert.Same(pair0.Value.Value, pair1.Value.Value);
Assert.False(pair0.IsReadOnly);
Assert.True(pair1.IsReadOnly);
// Change one value and verify the other is unchanged.
pair0.Value = "othervalue";
Assert.Equal("othervalue", pair0.Value);
Assert.Equal("value", pair1.Value);
Assert.Throws<InvalidOperationException>(() => { pair1.Value = "othervalue"; });
}
[Fact]
public void CopyFromReadOnly_NameAndValue_CopiedAsNonReadOnly()
{
var pair0 = new NameValueHeaderValue("name", "value");
var pair1 = pair0.CopyAsReadOnly();
var pair2 = pair1.Copy();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name.Value, pair1.Name.Value);
Assert.Same(pair0.Value.Value, pair1.Value.Value);
// Change one value and verify the other is unchanged.
pair2.Value = "othervalue";
Assert.Equal("othervalue", pair2.Value);
Assert.Equal("value", pair1.Value);
}
[Fact]
public void Value_CallSetterWithInvalidValues_Throw()
{
// Just verify that the setter calls the same validation the ctor invokes.
Assert.Throws<FormatException>(() => { var x = new NameValueHeaderValue("name"); x.Value = " x "; });
Assert.Throws<FormatException>(() => { var x = new NameValueHeaderValue("name"); x.Value = "x y"; });
}
[Fact]
public void ToString_UseNoValueAndTokenAndQuotedStringValues_SerializedCorrectly()
{
var nameValue = new NameValueHeaderValue("text", "token");
Assert.Equal("text=token", nameValue.ToString());
nameValue.Value = "\"quoted string\"";
Assert.Equal("text=\"quoted string\"", nameValue.ToString());
nameValue.Value = null;
Assert.Equal("text", nameValue.ToString());
nameValue.Value = string.Empty;
Assert.Equal("text", nameValue.ToString());
}
[Fact]
public void GetHashCode_ValuesUseDifferentValues_HashDiffersAccordingToRfc()
{
var nameValue1 = new NameValueHeaderValue("text");
var nameValue2 = new NameValueHeaderValue("text");
nameValue1.Value = null;
nameValue2.Value = null;
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
nameValue1.Value = "token";
nameValue2.Value = null;
Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode());
nameValue1.Value = "token";
nameValue2.Value = string.Empty;
Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode());
nameValue1.Value = null;
nameValue2.Value = string.Empty;
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
nameValue1.Value = "token";
nameValue2.Value = "TOKEN";
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
nameValue1.Value = "token";
nameValue2.Value = "token";
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
nameValue1.Value = "\"quoted string\"";
nameValue2.Value = "\"QUOTED STRING\"";
Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode());
nameValue1.Value = "\"quoted string\"";
nameValue2.Value = "\"quoted string\"";
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
}
[Fact]
public void GetHashCode_NameUseDifferentCasing_HashDiffersAccordingToRfc()
{
var nameValue1 = new NameValueHeaderValue("text");
var nameValue2 = new NameValueHeaderValue("TEXT");
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
}
[Fact]
public void Equals_ValuesUseDifferentValues_ValuesAreEqualOrDifferentAccordingToRfc()
{
var nameValue1 = new NameValueHeaderValue("text");
var nameValue2 = new NameValueHeaderValue("text");
nameValue1.Value = null;
nameValue2.Value = null;
Assert.True(nameValue1.Equals(nameValue2), "<null> vs. <null>.");
nameValue1.Value = "token";
nameValue2.Value = null;
Assert.False(nameValue1.Equals(nameValue2), "token vs. <null>.");
nameValue1.Value = null;
nameValue2.Value = "token";
Assert.False(nameValue1.Equals(nameValue2), "<null> vs. token.");
nameValue1.Value = string.Empty;
nameValue2.Value = "token";
Assert.False(nameValue1.Equals(nameValue2), "string.Empty vs. token.");
nameValue1.Value = null;
nameValue2.Value = string.Empty;
Assert.True(nameValue1.Equals(nameValue2), "<null> vs. string.Empty.");
nameValue1.Value = "token";
nameValue2.Value = "TOKEN";
Assert.True(nameValue1.Equals(nameValue2), "token vs. TOKEN.");
nameValue1.Value = "token";
nameValue2.Value = "token";
Assert.True(nameValue1.Equals(nameValue2), "token vs. token.");
nameValue1.Value = "\"quoted string\"";
nameValue2.Value = "\"QUOTED STRING\"";
Assert.False(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"QUOTED STRING\".");
nameValue1.Value = "\"quoted string\"";
nameValue2.Value = "\"quoted string\"";
Assert.True(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"quoted string\".");
Assert.False(nameValue1.Equals(null), "\"quoted string\" vs. <null>.");
}
[Fact]
public void Equals_NameUseDifferentCasing_ConsideredEqual()
{
var nameValue1 = new NameValueHeaderValue("text");
var nameValue2 = new NameValueHeaderValue("TEXT");
Assert.True(nameValue1.Equals(nameValue2), "text vs. TEXT.");
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidParse(" name = value ", new NameValueHeaderValue("name", "value"));
CheckValidParse(" name", new NameValueHeaderValue("name"));
CheckValidParse(" name ", new NameValueHeaderValue("name"));
CheckValidParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\""));
CheckValidParse("name=value", new NameValueHeaderValue("name", "value"));
CheckValidParse("name=\"quoted str\"", new NameValueHeaderValue("name", "\"quoted str\""));
CheckValidParse("name\t =va1ue", new NameValueHeaderValue("name", "va1ue"));
CheckValidParse("name= va*ue ", new NameValueHeaderValue("name", "va*ue"));
CheckValidParse("name=", new NameValueHeaderValue("name", ""));
}
[Fact]
public void Parse_SetOfInvalidValueStrings_Throws()
{
CheckInvalidParse("name[value");
CheckInvalidParse("name=value=");
CheckInvalidParse("name=会");
CheckInvalidParse("name==value");
CheckInvalidParse("name= va:ue");
CheckInvalidParse("=value");
CheckInvalidParse("name value");
CheckInvalidParse("name=,value");
CheckInvalidParse("会");
CheckInvalidParse(null);
CheckInvalidParse(string.Empty);
CheckInvalidParse(" ");
CheckInvalidParse(" ,,");
CheckInvalidParse(" , , name = value , ");
CheckInvalidParse(" name,");
CheckInvalidParse(" ,name=\"value\"");
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidTryParse(" name = value ", new NameValueHeaderValue("name", "value"));
CheckValidTryParse(" name", new NameValueHeaderValue("name"));
CheckValidTryParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\""));
CheckValidTryParse("name=value", new NameValueHeaderValue("name", "value"));
}
[Fact]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
{
CheckInvalidTryParse("name[value");
CheckInvalidTryParse("name=value=");
CheckInvalidTryParse("name=会");
CheckInvalidTryParse("name==value");
CheckInvalidTryParse("=value");
CheckInvalidTryParse("name value");
CheckInvalidTryParse("name=,value");
CheckInvalidTryParse("会");
CheckInvalidTryParse(null);
CheckInvalidTryParse(string.Empty);
CheckInvalidTryParse(" ");
CheckInvalidTryParse(" ,,");
CheckInvalidTryParse(" , , name = value , ");
CheckInvalidTryParse(" name,");
CheckInvalidTryParse(" ,name=\"value\"");
}
[Fact]
public void ParseList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"name=value1",
"",
" name = value2 ",
"\r\n name =value3\r\n ",
"name=\"value 4\"",
"name=\"value会5\"",
"name=value6,name=value7",
"name=\"value 8\", name= \"value 9\"",
};
var results = NameValueHeaderValue.ParseList(inputs);
var expectedResults = new[]
{
new NameValueHeaderValue("name", "value1"),
new NameValueHeaderValue("name", "value2"),
new NameValueHeaderValue("name", "value3"),
new NameValueHeaderValue("name", "\"value 4\""),
new NameValueHeaderValue("name", "\"value会5\""),
new NameValueHeaderValue("name", "value6"),
new NameValueHeaderValue("name", "value7"),
new NameValueHeaderValue("name", "\"value 8\""),
new NameValueHeaderValue("name", "\"value 9\""),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"name=value1",
"",
" name = value2 ",
"\r\n name =value3\r\n ",
"name=\"value 4\"",
"name=\"value会5\"",
"name=value6,name=value7",
"name=\"value 8\", name= \"value 9\"",
};
var results = NameValueHeaderValue.ParseStrictList(inputs);
var expectedResults = new[]
{
new NameValueHeaderValue("name", "value1"),
new NameValueHeaderValue("name", "value2"),
new NameValueHeaderValue("name", "value3"),
new NameValueHeaderValue("name", "\"value 4\""),
new NameValueHeaderValue("name", "\"value会5\""),
new NameValueHeaderValue("name", "value6"),
new NameValueHeaderValue("name", "value7"),
new NameValueHeaderValue("name", "\"value 8\""),
new NameValueHeaderValue("name", "\"value 9\""),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"name=value1",
"",
" name = value2 ",
"\r\n name =value3\r\n ",
"name=\"value 4\"",
"name=\"value会5\"",
"name=value6,name=value7",
"name=\"value 8\", name= \"value 9\"",
};
IList<NameValueHeaderValue> results;
Assert.True(NameValueHeaderValue.TryParseList(inputs, out results));
var expectedResults = new[]
{
new NameValueHeaderValue("name", "value1"),
new NameValueHeaderValue("name", "value2"),
new NameValueHeaderValue("name", "value3"),
new NameValueHeaderValue("name", "\"value 4\""),
new NameValueHeaderValue("name", "\"value会5\""),
new NameValueHeaderValue("name", "value6"),
new NameValueHeaderValue("name", "value7"),
new NameValueHeaderValue("name", "\"value 8\""),
new NameValueHeaderValue("name", "\"value 9\""),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"name=value1",
"",
" name = value2 ",
"\r\n name =value3\r\n ",
"name=\"value 4\"",
"name=\"value会5\"",
"name=value6,name=value7",
"name=\"value 8\", name= \"value 9\"",
};
IList<NameValueHeaderValue> results;
Assert.True(NameValueHeaderValue.TryParseStrictList(inputs, out results));
var expectedResults = new[]
{
new NameValueHeaderValue("name", "value1"),
new NameValueHeaderValue("name", "value2"),
new NameValueHeaderValue("name", "value3"),
new NameValueHeaderValue("name", "\"value 4\""),
new NameValueHeaderValue("name", "\"value会5\""),
new NameValueHeaderValue("name", "value6"),
new NameValueHeaderValue("name", "value7"),
new NameValueHeaderValue("name", "\"value 8\""),
new NameValueHeaderValue("name", "\"value 9\""),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseList_WithSomeInvlaidValues_ExcludesInvalidValues()
{
var inputs = new[]
{
"",
"name1=value1",
"name2",
" name3 = 3, value a",
"name4 =value4, name5 = value5 b",
"name6=\"value 6",
"name7=\"value会7\"",
"name8=value8,name9=value9",
"name10=\"value 10\", name11= \"value 11\"",
};
var results = NameValueHeaderValue.ParseList(inputs);
var expectedResults = new[]
{
new NameValueHeaderValue("name1", "value1"),
new NameValueHeaderValue("name2"),
new NameValueHeaderValue("name3", "3"),
new NameValueHeaderValue("a"),
new NameValueHeaderValue("name4", "value4"),
new NameValueHeaderValue("b"),
new NameValueHeaderValue("6"),
new NameValueHeaderValue("name7", "\"value会7\""),
new NameValueHeaderValue("name8", "value8"),
new NameValueHeaderValue("name9", "value9"),
new NameValueHeaderValue("name10", "\"value 10\""),
new NameValueHeaderValue("name11", "\"value 11\""),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseStrictList_WithSomeInvlaidValues_Throws()
{
var inputs = new[]
{
"",
"name1=value1",
"name2",
" name3 = 3, value a",
"name4 =value4, name5 = value5 b",
"name6=\"value 6",
"name7=\"value会7\"",
"name8=value8,name9=value9",
"name10=\"value 10\", name11= \"value 11\"",
};
Assert.Throws<FormatException>(() => NameValueHeaderValue.ParseStrictList(inputs));
}
[Fact]
public void TryParseList_WithSomeInvlaidValues_ExcludesInvalidValues()
{
var inputs = new[]
{
"",
"name1=value1",
"name2",
" name3 = 3, value a",
"name4 =value4, name5 = value5 b",
"name6=\"value 6",
"name7=\"value会7\"",
"name8=value8,name9=value9",
"name10=\"value 10\", name11= \"value 11\"",
};
IList<NameValueHeaderValue> results;
Assert.True(NameValueHeaderValue.TryParseList(inputs, out results));
var expectedResults = new[]
{
new NameValueHeaderValue("name1", "value1"),
new NameValueHeaderValue("name2"),
new NameValueHeaderValue("name3", "3"),
new NameValueHeaderValue("a"),
new NameValueHeaderValue("name4", "value4"),
new NameValueHeaderValue("b"),
new NameValueHeaderValue("6"),
new NameValueHeaderValue("name7", "\"value会7\""),
new NameValueHeaderValue("name8", "value8"),
new NameValueHeaderValue("name9", "value9"),
new NameValueHeaderValue("name10", "\"value 10\""),
new NameValueHeaderValue("name11", "\"value 11\""),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse()
{
var inputs = new[]
{
"",
"name1=value1",
"name2",
" name3 = 3, value a",
"name4 =value4, name5 = value5 b",
"name6=\"value 6",
"name7=\"value会7\"",
"name8=value8,name9=value9",
"name10=\"value 10\", name11= \"value 11\"",
};
IList<NameValueHeaderValue> results;
Assert.False(NameValueHeaderValue.TryParseStrictList(inputs, out results));
}
[Theory]
[InlineData("value", "value")]
[InlineData("\"value\"", "value")]
[InlineData("\"hello\\\\\"", "hello\\")]
[InlineData("\"hello\\\"\"", "hello\"")]
[InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")]
[InlineData("\"quoted value\"", "quoted value")]
[InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")]
[InlineData("\"hello\\\"", "hello\\")]
public void GetUnescapedValue_ReturnsExpectedValue(string input, string expected)
{
var header = new NameValueHeaderValue("test", input);
var actual = header.GetUnescapedValue();
Assert.Equal(expected, actual);
}
[Theory]
[InlineData("value", "value")]
[InlineData("23", "23")]
[InlineData(";;;", "\";;;\"")]
[InlineData("\"value\"", "\"value\"")]
[InlineData("\"assumes already encoded \\\"\"", "\"assumes already encoded \\\"\"")]
[InlineData("unquoted \"value", "\"unquoted \\\"value\"")]
[InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")]
// We have to assume that the input needs to be quoted here
[InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")]
[InlineData("\t", "\"\t\"")]
public void SetAndEscapeValue_ReturnsExpectedValue(string input, string expected)
{
var header = new NameValueHeaderValue("test");
header.SetAndEscapeValue(input);
var actual = header.Value;
Assert.Equal(expected, actual);
}
[Theory]
[InlineData("\n")]
[InlineData("\b")]
[InlineData("\r")]
public void SetAndEscapeValue_ThrowsOnInvalidValues(string input)
{
var header = new NameValueHeaderValue("test");
Assert.Throws<FormatException>(() => header.SetAndEscapeValue(input));
}
[Theory]
[InlineData("value")]
[InlineData("\"value\\\\morevalues\\\\evenmorevalues\"")]
[InlineData("\"quoted \\\"value\"")]
public void GetAndSetEncodeValueRoundTrip_ReturnsExpectedValue(string input)
{
var header = new NameValueHeaderValue("test");
header.Value = input;
var valueHeader = header.GetUnescapedValue();
header.SetAndEscapeValue(valueHeader);
var actual = header.Value;
Assert.Equal(input, actual);
}
[Theory]
[InlineData("val\\nue")]
[InlineData("val\\bue")]
public void OverescapingValuesDoNotRoundTrip(string input)
{
var header = new NameValueHeaderValue("test");
header.SetAndEscapeValue(input);
var valueHeader = header.GetUnescapedValue();
var actual = header.Value;
Assert.NotEqual(input, actual);
}
#region Helper methods
private void CheckValidParse(string input, NameValueHeaderValue expectedResult)
{
var result = NameValueHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckInvalidParse(string input)
{
Assert.Throws<FormatException>(() => NameValueHeaderValue.Parse(input));
}
private void CheckValidTryParse(string input, NameValueHeaderValue expectedResult)
{
NameValueHeaderValue result = null;
Assert.True(NameValueHeaderValue.TryParse(input, out result));
Assert.Equal(expectedResult, result);
}
private void CheckInvalidTryParse(string input)
{
NameValueHeaderValue result = null;
Assert.False(NameValueHeaderValue.TryParse(input, out result));
Assert.Null(result);
}
private static void CheckValue(string value)
{
var nameValue = new NameValueHeaderValue("text", value);
Assert.Equal(value, nameValue.Value);
}
private static void AssertFormatException(string name, string value)
{
Assert.Throws<FormatException>(() => new NameValueHeaderValue(name, value));
}
#endregion
}
}

View File

@ -0,0 +1,174 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class RangeConditionHeaderValueTest
{
[Fact]
public void Ctor_EntityTagOverload_MatchExpectation()
{
var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\""));
Assert.Equal(new EntityTagHeaderValue("\"x\""), rangeCondition.EntityTag);
Assert.Null(rangeCondition.LastModified);
EntityTagHeaderValue input = null;
Assert.Throws<ArgumentNullException>(() => new RangeConditionHeaderValue(input));
}
[Fact]
public void Ctor_EntityTagStringOverload_MatchExpectation()
{
var rangeCondition = new RangeConditionHeaderValue("\"y\"");
Assert.Equal(new EntityTagHeaderValue("\"y\""), rangeCondition.EntityTag);
Assert.Null(rangeCondition.LastModified);
Assert.Throws<ArgumentException>(() => new RangeConditionHeaderValue((string)null));
}
[Fact]
public void Ctor_DateOverload_MatchExpectation()
{
var rangeCondition = new RangeConditionHeaderValue(
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
Assert.Null(rangeCondition.EntityTag);
Assert.Equal(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero), rangeCondition.LastModified);
}
[Fact]
public void ToString_UseDifferentrangeConditions_AllSerializedCorrectly()
{
var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\""));
Assert.Equal("\"x\"", rangeCondition.ToString());
rangeCondition = new RangeConditionHeaderValue(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
Assert.Equal("Thu, 15 Jul 2010 12:33:57 GMT", rangeCondition.ToString());
}
[Fact]
public void GetHashCode_UseSameAndDifferentrangeConditions_SameOrDifferentHashCodes()
{
var rangeCondition1 = new RangeConditionHeaderValue("\"x\"");
var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\""));
var rangeCondition3 = new RangeConditionHeaderValue(
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
var rangeCondition4 = new RangeConditionHeaderValue(
new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero));
var rangeCondition5 = new RangeConditionHeaderValue(
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
var rangeCondition6 = new RangeConditionHeaderValue(
new EntityTagHeaderValue("\"x\"", true));
Assert.Equal(rangeCondition1.GetHashCode(), rangeCondition2.GetHashCode());
Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition3.GetHashCode());
Assert.NotEqual(rangeCondition3.GetHashCode(), rangeCondition4.GetHashCode());
Assert.Equal(rangeCondition3.GetHashCode(), rangeCondition5.GetHashCode());
Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition6.GetHashCode());
}
[Fact]
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
{
var rangeCondition1 = new RangeConditionHeaderValue("\"x\"");
var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\""));
var rangeCondition3 = new RangeConditionHeaderValue(
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
var rangeCondition4 = new RangeConditionHeaderValue(
new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero));
var rangeCondition5 = new RangeConditionHeaderValue(
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
var rangeCondition6 = new RangeConditionHeaderValue(
new EntityTagHeaderValue("\"x\"", true));
Assert.False(rangeCondition1.Equals(null), "\"x\" vs. <null>");
Assert.True(rangeCondition1.Equals(rangeCondition2), "\"x\" vs. \"x\"");
Assert.False(rangeCondition1.Equals(rangeCondition3), "\"x\" vs. date");
Assert.False(rangeCondition3.Equals(rangeCondition1), "date vs. \"x\"");
Assert.False(rangeCondition3.Equals(rangeCondition4), "date vs. different date");
Assert.True(rangeCondition3.Equals(rangeCondition5), "date vs. date");
Assert.False(rangeCondition1.Equals(rangeCondition6), "\"x\" vs. W/\"x\"");
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidParse(" \"x\" ", new RangeConditionHeaderValue("\"x\""));
CheckValidParse(" Sun, 06 Nov 1994 08:49:37 GMT ",
new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero)));
CheckValidParse("Wed, 09 Nov 1994 08:49:37 GMT",
new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 9, 8, 49, 37, TimeSpan.Zero)));
CheckValidParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true)));
CheckValidParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true)));
CheckValidParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"")));
}
[Theory]
[InlineData("\"x\" ,")] // no delimiter allowed
[InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed
[InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")]
[InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")]
[InlineData(null)]
[InlineData("")]
[InlineData(" Wed 09 Nov 1994 08:49:37 GMT")]
[InlineData("\"x")]
[InlineData("Wed, 09 Nov")]
[InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")]
[InlineData("\"x\",")]
[InlineData("Wed 09 Nov 1994 08:49:37 GMT,")]
public void Parse_SetOfInvalidValueStrings_Throws(string input)
{
Assert.Throws<FormatException>(() => RangeConditionHeaderValue.Parse(input));
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidTryParse(" \"x\" ", new RangeConditionHeaderValue("\"x\""));
CheckValidTryParse(" Sun, 06 Nov 1994 08:49:37 GMT ",
new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero)));
CheckValidTryParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true)));
CheckValidTryParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true)));
CheckValidTryParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"")));
}
[Theory]
[InlineData("\"x\" ,")] // no delimiter allowed
[InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed
[InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")]
[InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")]
[InlineData(null)]
[InlineData("")]
[InlineData(" Wed 09 Nov 1994 08:49:37 GMT")]
[InlineData("\"x")]
[InlineData("Wed, 09 Nov")]
[InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")]
[InlineData("\"x\",")]
[InlineData("Wed 09 Nov 1994 08:49:37 GMT,")]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input)
{
RangeConditionHeaderValue result = null;
Assert.False(RangeConditionHeaderValue.TryParse(input, out result));
Assert.Null(result);
}
#region Helper methods
private void CheckValidParse(string input, RangeConditionHeaderValue expectedResult)
{
var result = RangeConditionHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckValidTryParse(string input, RangeConditionHeaderValue expectedResult)
{
RangeConditionHeaderValue result = null;
Assert.True(RangeConditionHeaderValue.TryParse(input, out result));
Assert.Equal(expectedResult, result);
}
#endregion
}
}

View File

@ -0,0 +1,183 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class RangeHeaderValueTest
{
[Fact]
public void Ctor_InvalidRange_Throw()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new RangeHeaderValue(5, 2));
}
[Fact]
public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation()
{
var range = new RangeHeaderValue();
range.Unit = "myunit";
Assert.Equal("myunit", range.Unit);
Assert.Throws<ArgumentException>(() => range.Unit = null);
Assert.Throws<ArgumentException>(() => range.Unit = "");
Assert.Throws<FormatException>(() => range.Unit = " x");
Assert.Throws<FormatException>(() => range.Unit = "x ");
Assert.Throws<FormatException>(() => range.Unit = "x y");
}
[Fact]
public void ToString_UseDifferentRanges_AllSerializedCorrectly()
{
var range = new RangeHeaderValue();
range.Unit = "myunit";
range.Ranges.Add(new RangeItemHeaderValue(1, 3));
Assert.Equal("myunit=1-3", range.ToString());
range.Ranges.Add(new RangeItemHeaderValue(5, null));
range.Ranges.Add(new RangeItemHeaderValue(null, 17));
Assert.Equal("myunit=1-3, 5-, -17", range.ToString());
}
[Fact]
public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes()
{
var range1 = new RangeHeaderValue(1, 2);
var range2 = new RangeHeaderValue(1, 2);
range2.Unit = "BYTES";
var range3 = new RangeHeaderValue(1, null);
var range4 = new RangeHeaderValue(null, 2);
var range5 = new RangeHeaderValue();
range5.Ranges.Add(new RangeItemHeaderValue(1, 2));
range5.Ranges.Add(new RangeItemHeaderValue(3, 4));
var range6 = new RangeHeaderValue();
range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5
range6.Ranges.Add(new RangeItemHeaderValue(1, 2));
Assert.Equal(range1.GetHashCode(), range2.GetHashCode());
Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode());
Assert.NotEqual(range1.GetHashCode(), range4.GetHashCode());
Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode());
Assert.Equal(range5.GetHashCode(), range6.GetHashCode());
}
[Fact]
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
{
var range1 = new RangeHeaderValue(1, 2);
var range2 = new RangeHeaderValue(1, 2);
range2.Unit = "BYTES";
var range3 = new RangeHeaderValue(1, null);
var range4 = new RangeHeaderValue(null, 2);
var range5 = new RangeHeaderValue();
range5.Ranges.Add(new RangeItemHeaderValue(1, 2));
range5.Ranges.Add(new RangeItemHeaderValue(3, 4));
var range6 = new RangeHeaderValue();
range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5
range6.Ranges.Add(new RangeItemHeaderValue(1, 2));
var range7 = new RangeHeaderValue(1, 2);
range7.Unit = "other";
Assert.False(range1.Equals(null), "bytes=1-2 vs. <null>");
Assert.True(range1.Equals(range2), "bytes=1-2 vs. BYTES=1-2");
Assert.False(range1.Equals(range3), "bytes=1-2 vs. bytes=1-");
Assert.False(range1.Equals(range4), "bytes=1-2 vs. bytes=-2");
Assert.False(range1.Equals(range5), "bytes=1-2 vs. bytes=1-2,3-4");
Assert.True(range5.Equals(range6), "bytes=1-2,3-4 vs. bytes=3-4,1-2");
Assert.False(range1.Equals(range7), "bytes=1-2 vs. other=1-2");
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidParse(" bytes=1-2 ", new RangeHeaderValue(1, 2));
var expected = new RangeHeaderValue();
expected.Unit = "custom";
expected.Ranges.Add(new RangeItemHeaderValue(null, 5));
expected.Ranges.Add(new RangeItemHeaderValue(1, 4));
CheckValidParse("custom = - 5 , 1 - 4 ,,", expected);
expected = new RangeHeaderValue();
expected.Unit = "custom";
expected.Ranges.Add(new RangeItemHeaderValue(1, 2));
CheckValidParse(" custom = 1 - 2", expected);
expected = new RangeHeaderValue();
expected.Ranges.Add(new RangeItemHeaderValue(1, 2));
expected.Ranges.Add(new RangeItemHeaderValue(3, null));
expected.Ranges.Add(new RangeItemHeaderValue(null, 4));
CheckValidParse("bytes =1-2,,3-, , ,-4,,", expected);
}
[Fact]
public void Parse_SetOfInvalidValueStrings_Throws()
{
CheckInvalidParse("bytes=1-2x"); // only delimiter ',' allowed after last range
CheckInvalidParse("x bytes=1-2");
CheckInvalidParse("bytes=1-2.4");
CheckInvalidParse(null);
CheckInvalidParse(string.Empty);
CheckInvalidParse("bytes=1");
CheckInvalidParse("bytes=");
CheckInvalidParse("bytes");
CheckInvalidParse("bytes 1-2");
CheckInvalidParse("bytes= ,,, , ,,");
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidTryParse(" bytes=1-2 ", new RangeHeaderValue(1, 2));
var expected = new RangeHeaderValue();
expected.Unit = "custom";
expected.Ranges.Add(new RangeItemHeaderValue(null, 5));
expected.Ranges.Add(new RangeItemHeaderValue(1, 4));
CheckValidTryParse("custom = - 5 , 1 - 4 ,,", expected);
}
[Fact]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
{
CheckInvalidTryParse("bytes=1-2x"); // only delimiter ',' allowed after last range
CheckInvalidTryParse("x bytes=1-2");
CheckInvalidTryParse("bytes=1-2.4");
CheckInvalidTryParse(null);
CheckInvalidTryParse(string.Empty);
}
#region Helper methods
private void CheckValidParse(string input, RangeHeaderValue expectedResult)
{
var result = RangeHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckInvalidParse(string input)
{
Assert.Throws<FormatException>(() => RangeHeaderValue.Parse(input));
}
private void CheckValidTryParse(string input, RangeHeaderValue expectedResult)
{
RangeHeaderValue result = null;
Assert.True(RangeHeaderValue.TryParse(input, out result));
Assert.Equal(expectedResult, result);
}
private void CheckInvalidTryParse(string input)
{
RangeHeaderValue result = null;
Assert.False(RangeHeaderValue.TryParse(input, out result));
Assert.Null(result);
}
#endregion
}
}

View File

@ -0,0 +1,162 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class RangeItemHeaderValueTest
{
[Fact]
public void Ctor_BothValuesNull_Throw()
{
Assert.Throws<ArgumentException>(() => new RangeItemHeaderValue(null, null));
}
[Fact]
public void Ctor_FromValueNegative_Throw()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new RangeItemHeaderValue(-1, null));
}
[Fact]
public void Ctor_FromGreaterThanToValue_Throw()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new RangeItemHeaderValue(2, 1));
}
[Fact]
public void Ctor_ToValueNegative_Throw()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new RangeItemHeaderValue(null, -1));
}
[Fact]
public void Ctor_ValidFormat_SuccessfullyCreated()
{
var rangeItem = new RangeItemHeaderValue(1, 2);
Assert.Equal(1, rangeItem.From);
Assert.Equal(2, rangeItem.To);
}
[Fact]
public void ToString_UseDifferentRangeItems_AllSerializedCorrectly()
{
// Make sure ToString() doesn't add any separators.
var rangeItem = new RangeItemHeaderValue(1000000000, 2000000000);
Assert.Equal("1000000000-2000000000", rangeItem.ToString());
rangeItem = new RangeItemHeaderValue(5, null);
Assert.Equal("5-", rangeItem.ToString());
rangeItem = new RangeItemHeaderValue(null, 10);
Assert.Equal("-10", rangeItem.ToString());
}
[Fact]
public void GetHashCode_UseSameAndDifferentRangeItems_SameOrDifferentHashCodes()
{
var rangeItem1 = new RangeItemHeaderValue(1, 2);
var rangeItem2 = new RangeItemHeaderValue(1, null);
var rangeItem3 = new RangeItemHeaderValue(null, 2);
var rangeItem4 = new RangeItemHeaderValue(2, 2);
var rangeItem5 = new RangeItemHeaderValue(1, 2);
Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem2.GetHashCode());
Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem3.GetHashCode());
Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem4.GetHashCode());
Assert.Equal(rangeItem1.GetHashCode(), rangeItem5.GetHashCode());
}
[Fact]
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
{
var rangeItem1 = new RangeItemHeaderValue(1, 2);
var rangeItem2 = new RangeItemHeaderValue(1, null);
var rangeItem3 = new RangeItemHeaderValue(null, 2);
var rangeItem4 = new RangeItemHeaderValue(2, 2);
var rangeItem5 = new RangeItemHeaderValue(1, 2);
Assert.False(rangeItem1.Equals(rangeItem2), "1-2 vs. 1-.");
Assert.False(rangeItem2.Equals(rangeItem1), "1- vs. 1-2.");
Assert.False(rangeItem1.Equals(null), "1-2 vs. null.");
Assert.False(rangeItem1.Equals(rangeItem3), "1-2 vs. -2.");
Assert.False(rangeItem3.Equals(rangeItem1), "-2 vs. 1-2.");
Assert.False(rangeItem1.Equals(rangeItem4), "1-2 vs. 2-2.");
Assert.True(rangeItem1.Equals(rangeItem5), "1-2 vs. 1-2.");
}
[Fact]
public void TryParse_DifferentValidScenarios_AllReturnNonZero()
{
CheckValidTryParse("1-2", 1, 2);
CheckValidTryParse(" 1-2", 1, 2);
CheckValidTryParse("0-0", 0, 0);
CheckValidTryParse(" 1-", 1, null);
CheckValidTryParse(" -2", null, 2);
CheckValidTryParse(" 684684 - 123456789012345 ", 684684, 123456789012345);
// The separator doesn't matter. It only parses until the first non-whitespace
CheckValidTryParse(" 1 - 2 ,", 1, 2);
CheckValidTryParse(",,1-2, 3 - , , -6 , ,,", new Tuple<long?, long?>(1, 2), new Tuple<long?, long?>(3, null),
new Tuple<long?, long?>(null, 6));
CheckValidTryParse("1-2,", new Tuple<long?, long?>(1, 2));
CheckValidTryParse("1-", new Tuple<long?, long?>(1, null));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(",,")]
[InlineData("1")]
[InlineData("1-2,3")]
[InlineData("1--2")]
[InlineData("1,-2")]
[InlineData("-")]
[InlineData("--")]
[InlineData("2-1")]
[InlineData("12345678901234567890123-")] // >>Int64.MaxValue
[InlineData("-12345678901234567890123")] // >>Int64.MaxValue
[InlineData("9999999999999999999-")] // 19-digit numbers outside the Int64 range.
[InlineData("-9999999999999999999")] // 19-digit numbers outside the Int64 range.
public void TryParse_DifferentInvalidScenarios_AllReturnFalse(string input)
{
RangeHeaderValue result;
Assert.False(RangeHeaderValue.TryParse("byte=" + input, out result));
}
private static void CheckValidTryParse(string input, long? expectedFrom, long? expectedTo)
{
RangeHeaderValue result;
Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input);
var ranges = result.Ranges.ToArray();
Assert.Single(ranges);
var range = ranges.First();
Assert.Equal(expectedFrom, range.From);
Assert.Equal(expectedTo, range.To);
}
private static void CheckValidTryParse(string input, params Tuple<long?, long?>[] expectedRanges)
{
RangeHeaderValue result;
Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input);
var ranges = result.Ranges.ToArray();
Assert.Equal(expectedRanges.Length, ranges.Length);
for (int i = 0; i < expectedRanges.Length; i++)
{
Assert.Equal(expectedRanges[i].Item1, ranges[i].From);
Assert.Equal(expectedRanges[i].Item2, ranges[i].To);
}
}
}
}

View File

@ -0,0 +1,429 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class SetCookieHeaderValueTest
{
public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
{
get
{
var dataset = new TheoryData<SetCookieHeaderValue, string>();
var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3")
{
Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
SameSite = SameSiteMode.Strict,
HttpOnly = true,
MaxAge = TimeSpan.FromDays(1),
Path = "path1",
Secure = true
};
dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly");
var header2 = new SetCookieHeaderValue("name2", "");
dataset.Add(header2, "name2=");
var header3 = new SetCookieHeaderValue("name2", "value2");
dataset.Add(header3, "name2=value2");
var header4 = new SetCookieHeaderValue("name4", "value4")
{
MaxAge = TimeSpan.FromDays(1),
};
dataset.Add(header4, "name4=value4; max-age=86400");
var header5 = new SetCookieHeaderValue("name5", "value5")
{
Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
};
dataset.Add(header5, "name5=value5; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1");
var header6 = new SetCookieHeaderValue("name6", "value6")
{
SameSite = SameSiteMode.Lax,
};
dataset.Add(header6, "name6=value6; samesite=lax");
var header7 = new SetCookieHeaderValue("name7", "value7")
{
SameSite = SameSiteMode.None,
};
dataset.Add(header7, "name7=value7");
return dataset;
}
}
public static TheoryData<string> InvalidSetCookieHeaderDataSet
{
get
{
return new TheoryData<string>
{
"expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1",
"name=value; expires=Sun, 06 Nov 1994 08:49:37 ZZZ; max-age=86400; domain=domain1",
"name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=-86400; domain=domain1",
};
}
}
public static TheoryData<string> InvalidCookieNames
{
get
{
return new TheoryData<string>
{
"<acb>",
"{acb}",
"[acb]",
"\"acb\"",
"a,b",
"a;b",
"a\\b",
};
}
}
public static TheoryData<string> InvalidCookieValues
{
get
{
return new TheoryData<string>
{
{ "\"" },
{ "a,b" },
{ "a;b" },
{ "a\\b" },
{ "\"abc" },
{ "a\"bc" },
{ "abc\"" },
};
}
}
public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieHeaderDataSet
{
get
{
var dataset = new TheoryData<IList<SetCookieHeaderValue>, string[]>();
var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3")
{
Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
SameSite = SameSiteMode.Strict,
HttpOnly = true,
MaxAge = TimeSpan.FromDays(1),
Path = "path1",
Secure = true
};
var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly";
var header2 = new SetCookieHeaderValue("name2", "value2");
var string2 = "name2=value2";
var header3 = new SetCookieHeaderValue("name3", "value3")
{
MaxAge = TimeSpan.FromDays(1),
};
var string3 = "name3=value3; max-age=86400";
var header4 = new SetCookieHeaderValue("name4", "value4")
{
Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
};
var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1";
var header5 = new SetCookieHeaderValue("name5", "value5")
{
SameSite = SameSiteMode.Lax
};
var string5a = "name5=value5; samesite=lax";
var string5b = "name5=value5; samesite=Lax";
var header6 = new SetCookieHeaderValue("name6", "value6")
{
SameSite = SameSiteMode.Strict
};
var string6a = "name6=value6; samesite";
var string6b = "name6=value6; samesite=Strict";
var string6c = "name6=value6; samesite=invalid";
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 });
dataset.Add(new[] { header2 }.ToList(), new[] { string2 });
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 });
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 });
dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + ", " + string1 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
dataset.Add(new[] { header5 }.ToList(), new[] { string5a });
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
dataset.Add(new[] { header6 }.ToList(), new[] { string6a });
dataset.Add(new[] { header6 }.ToList(), new[] { string6b });
dataset.Add(new[] { header6 }.ToList(), new[] { string6c });
return dataset;
}
}
public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListWithInvalidSetCookieHeaderDataSet
{
get
{
var dataset = new TheoryData<IList<SetCookieHeaderValue>, string[]>();
var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3")
{
Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
SameSite = SameSiteMode.Strict,
HttpOnly = true,
MaxAge = TimeSpan.FromDays(1),
Path = "path1",
Secure = true
};
var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=Strict; httponly";
var header2 = new SetCookieHeaderValue("name2", "value2");
var string2 = "name2=value2";
var header3 = new SetCookieHeaderValue("name3", "value3")
{
MaxAge = TimeSpan.FromDays(1),
};
var string3 = "name3=value3; max-age=86400";
var header4 = new SetCookieHeaderValue("name4", "value4")
{
Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
};
var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1;";
var invalidString1 = "ipt={\"v\":{\"L\":3},\"pt:{\"d\":3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}";
var invalidHeader2a = new SetCookieHeaderValue("expires", "Sun");
var invalidHeader2b = new SetCookieHeaderValue("domain", "domain1");
var invalidString2 = "ipt={\"v\":{\"L\":3},\"pt\":{d\":3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1";
var invalidHeader3 = new SetCookieHeaderValue("domain", "domain1")
{
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
};
var invalidString3 = "ipt={\"v\":{\"L\":3},\"pt\":{\"d:3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}; domain=domain1; expires=Sun, 06 Nov 1994 08:49:37 GMT";
dataset.Add(null, new[] { invalidString1 });
dataset.Add(new[] { invalidHeader2a, invalidHeader2b }.ToList(), new[] { invalidString2 });
dataset.Add(new[] { invalidHeader3 }.ToList(), new[] { invalidString3 });
dataset.Add(new[] { header1 }.ToList(), new[] { string1, invalidString1 });
dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ",", " , ", string1 });
dataset.Add(new[] { header1 }.ToList(), new[] { string1 + ", " + invalidString1 });
dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + ", " + string1 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { invalidString1, string1, string2, string3, string4 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, invalidString1, string2, string3, string4 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, invalidString1, string3, string4 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, invalidString1, string4 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4, invalidString1 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", invalidString1, string1, string2, string3, string4) });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, invalidString1, string2, string3, string4) });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, invalidString1, string3, string4) });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, invalidString1, string4) });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4, invalidString1) });
return dataset;
}
}
[Fact]
public void SetCookieHeaderValue_CtorThrowsOnNullName()
{
Assert.Throws<ArgumentNullException>(() => new SetCookieHeaderValue(null, "value"));
}
[Theory]
[MemberData(nameof(InvalidCookieNames))]
public void SetCookieHeaderValue_CtorThrowsOnInvalidName(string name)
{
Assert.Throws<ArgumentException>(() => new SetCookieHeaderValue(name, "value"));
}
[Theory]
[MemberData(nameof(InvalidCookieValues))]
public void SetCookieHeaderValue_CtorThrowsOnInvalidValue(string value)
{
Assert.Throws<ArgumentException>(() => new SetCookieHeaderValue("name", value));
}
[Fact]
public void SetCookieHeaderValue_Ctor1_InitializesCorrectly()
{
var header = new SetCookieHeaderValue("cookie");
Assert.Equal("cookie", header.Name);
Assert.Equal(string.Empty, header.Value);
}
[Theory]
[InlineData("name", "")]
[InlineData("name", "value")]
[InlineData("name", "\"acb\"")]
public void SetCookieHeaderValue_Ctor2InitializesCorrectly(string name, string value)
{
var header = new SetCookieHeaderValue(name, value);
Assert.Equal(name, header.Name);
Assert.Equal(value, header.Value);
}
[Fact]
public void SetCookieHeaderValue_Value()
{
var cookie = new SetCookieHeaderValue("name");
Assert.Equal(string.Empty, cookie.Value);
cookie.Value = "value1";
Assert.Equal("value1", cookie.Value);
}
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string expectedValue)
{
Assert.Equal(expectedValue, input.ToString());
}
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
{
var builder = new StringBuilder();
input.AppendToStringBuilder(builder);
Assert.Equal(expectedValue, builder.ToString());
}
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
{
var header = SetCookieHeaderValue.Parse(expectedValue);
Assert.Equal(cookie, header);
Assert.Equal(expectedValue, header.ToString());
}
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
{
Assert.True(SetCookieHeaderValue.TryParse(expectedValue, out var header));
Assert.Equal(cookie, header);
Assert.Equal(expectedValue, header.ToString());
}
[Theory]
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)
{
Assert.Throws<FormatException>(() => SetCookieHeaderValue.Parse(value));
}
[Theory]
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_TryParse_RejectsInvalidValues(string value)
{
Assert.False(SetCookieHeaderValue.TryParse(value, out var _));
}
[Theory]
[MemberData(nameof(ListOfSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_ParseList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input)
{
var results = SetCookieHeaderValue.ParseList(input);
Assert.Equal(cookies, results);
}
[Theory]
[MemberData(nameof(ListOfSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_TryParseList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input)
{
bool result = SetCookieHeaderValue.TryParseList(input, out var results);
Assert.True(result);
Assert.Equal(cookies, results);
}
[Theory]
[MemberData(nameof(ListOfSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_ParseStrictList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input)
{
var results = SetCookieHeaderValue.ParseStrictList(input);
Assert.Equal(cookies, results);
}
[Theory]
[MemberData(nameof(ListOfSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input)
{
bool result = SetCookieHeaderValue.TryParseStrictList(input, out var results);
Assert.True(result);
Assert.Equal(cookies, results);
}
[Theory]
[MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_ParseList_ExcludesInvalidValues(IList<SetCookieHeaderValue> cookies, string[] input)
{
var results = SetCookieHeaderValue.ParseList(input);
// ParseList aways returns a list, even if empty. TryParseList may return null (via out).
Assert.Equal(cookies ?? new List<SetCookieHeaderValue>(), results);
}
[Theory]
[MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_TryParseList_ExcludesInvalidValues(IList<SetCookieHeaderValue> cookies, string[] input)
{
bool result = SetCookieHeaderValue.TryParseList(input, out var results);
Assert.Equal(cookies, results);
Assert.Equal(cookies?.Count > 0, result);
}
[Theory]
[MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues(
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
IList<SetCookieHeaderValue> cookies,
#pragma warning restore xUnit1026 // Theory methods should use all of their parameters
string[] input)
{
Assert.Throws<FormatException>(() => SetCookieHeaderValue.ParseStrictList(input));
}
[Theory]
[MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_TryParseStrictList_FailsForAnyInvalidValues(
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
IList<SetCookieHeaderValue> cookies,
#pragma warning restore xUnit1026 // Theory methods should use all of their parameters
string[] input)
{
bool result = SetCookieHeaderValue.TryParseStrictList(input, out var results);
Assert.Null(results);
Assert.False(result);
}
}
}

View File

@ -0,0 +1,64 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class StringWithQualityHeaderValueComparerTest
{
public static TheoryData<string[], string[]> StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues
{
get
{
return new TheoryData<string[], string[]>
{
{
new string[]
{
"text",
"text;q=1.0",
"text",
"text;q=0",
"*;q=0.8",
"*;q=1",
"text;q=0.8",
"*;q=0.6",
"text;q=1.0",
"*;q=0.4",
"text;q=0.6",
},
new string[]
{
"text",
"text;q=1.0",
"text",
"text;q=1.0",
"*;q=1",
"text;q=0.8",
"*;q=0.8",
"text;q=0.6",
"*;q=0.6",
"*;q=0.4",
"text;q=0",
}
}
};
}
}
[Theory]
[MemberData(nameof(StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues))]
public void SortStringWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable<string> unsorted, IEnumerable<string> expectedSorted)
{
var unsortedValues = StringWithQualityHeaderValue.ParseList(unsorted.ToList());
var expectedSortedValues = StringWithQualityHeaderValue.ParseList(expectedSorted.ToList());
var actualSorted = unsortedValues.OrderByDescending(k => k, StringWithQualityHeaderValueComparer.QualityComparer).ToList();
Assert.True(expectedSortedValues.SequenceEqual(actualSorted));
}
}
}

View File

@ -0,0 +1,498 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.Net.Http.Headers
{
public class StringWithQualityHeaderValueTest
{
[Fact]
public void Ctor_StringOnlyOverload_MatchExpectation()
{
var value = new StringWithQualityHeaderValue("token");
Assert.Equal("token", value.Value);
Assert.Null(value.Quality);
Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue(null));
Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue(""));
Assert.Throws<FormatException>(() => new StringWithQualityHeaderValue("in valid"));
}
[Fact]
public void Ctor_StringWithQualityOverload_MatchExpectation()
{
var value = new StringWithQualityHeaderValue("token", 0.5);
Assert.Equal("token", value.Value);
Assert.Equal(0.5, value.Quality);
Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue(null, 0.1));
Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue("", 0.1));
Assert.Throws<FormatException>(() => new StringWithQualityHeaderValue("in valid", 0.1));
Assert.Throws<ArgumentOutOfRangeException>(() => new StringWithQualityHeaderValue("t", 1.1));
Assert.Throws<ArgumentOutOfRangeException>(() => new StringWithQualityHeaderValue("t", -0.1));
}
[Fact]
public void ToString_UseDifferentValues_AllSerializedCorrectly()
{
var value = new StringWithQualityHeaderValue("token");
Assert.Equal("token", value.ToString());
value = new StringWithQualityHeaderValue("token", 0.1);
Assert.Equal("token; q=0.1", value.ToString());
value = new StringWithQualityHeaderValue("token", 0);
Assert.Equal("token; q=0.0", value.ToString());
value = new StringWithQualityHeaderValue("token", 1);
Assert.Equal("token; q=1.0", value.ToString());
// Note that the quality value gets rounded
value = new StringWithQualityHeaderValue("token", 0.56789);
Assert.Equal("token; q=0.568", value.ToString());
}
[Fact]
public void GetHashCode_UseSameAndDifferentValues_SameOrDifferentHashCodes()
{
var value1 = new StringWithQualityHeaderValue("t", 0.123);
var value2 = new StringWithQualityHeaderValue("t", 0.123);
var value3 = new StringWithQualityHeaderValue("T", 0.123);
var value4 = new StringWithQualityHeaderValue("t");
var value5 = new StringWithQualityHeaderValue("x", 0.123);
var value6 = new StringWithQualityHeaderValue("t", 0.5);
var value7 = new StringWithQualityHeaderValue("t", 0.1234);
var value8 = new StringWithQualityHeaderValue("T");
var value9 = new StringWithQualityHeaderValue("x");
Assert.Equal(value1.GetHashCode(), value2.GetHashCode());
Assert.Equal(value1.GetHashCode(), value3.GetHashCode());
Assert.NotEqual(value1.GetHashCode(), value4.GetHashCode());
Assert.NotEqual(value1.GetHashCode(), value5.GetHashCode());
Assert.NotEqual(value1.GetHashCode(), value6.GetHashCode());
Assert.NotEqual(value1.GetHashCode(), value7.GetHashCode());
Assert.Equal(value4.GetHashCode(), value8.GetHashCode());
Assert.NotEqual(value4.GetHashCode(), value9.GetHashCode());
}
[Fact]
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
{
var value1 = new StringWithQualityHeaderValue("t", 0.123);
var value2 = new StringWithQualityHeaderValue("t", 0.123);
var value3 = new StringWithQualityHeaderValue("T", 0.123);
var value4 = new StringWithQualityHeaderValue("t");
var value5 = new StringWithQualityHeaderValue("x", 0.123);
var value6 = new StringWithQualityHeaderValue("t", 0.5);
var value7 = new StringWithQualityHeaderValue("t", 0.1234);
var value8 = new StringWithQualityHeaderValue("T");
var value9 = new StringWithQualityHeaderValue("x");
Assert.False(value1.Equals(null), "t; q=0.123 vs. <null>");
Assert.True(value1.Equals(value2), "t; q=0.123 vs. t; q=0.123");
Assert.True(value1.Equals(value3), "t; q=0.123 vs. T; q=0.123");
Assert.False(value1.Equals(value4), "t; q=0.123 vs. t");
Assert.False(value4.Equals(value1), "t vs. t; q=0.123");
Assert.False(value1.Equals(value5), "t; q=0.123 vs. x; q=0.123");
Assert.False(value1.Equals(value6), "t; q=0.123 vs. t; q=0.5");
Assert.False(value1.Equals(value7), "t; q=0.123 vs. t; q=0.1234");
Assert.True(value4.Equals(value8), "t vs. T");
Assert.False(value4.Equals(value9), "t vs. T");
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidParse("text", new StringWithQualityHeaderValue("text"));
CheckValidParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5));
CheckValidParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5));
CheckValidParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5));
CheckValidParse(" text ", new StringWithQualityHeaderValue("text"));
CheckValidParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123));
CheckValidParse(" text ; q = 0.123 ", new StringWithQualityHeaderValue("text", 0.123));
CheckValidParse("text;q=1 ", new StringWithQualityHeaderValue("text", 1));
CheckValidParse("*", new StringWithQualityHeaderValue("*"));
CheckValidParse("*;q=0.7", new StringWithQualityHeaderValue("*", 0.7));
CheckValidParse(" t", new StringWithQualityHeaderValue("t"));
CheckValidParse("t;q=0.", new StringWithQualityHeaderValue("t", 0));
CheckValidParse("t;q=1.", new StringWithQualityHeaderValue("t", 1));
CheckValidParse("t;q=1.000", new StringWithQualityHeaderValue("t", 1));
CheckValidParse("t;q=0.12345678", new StringWithQualityHeaderValue("t", 0.12345678));
CheckValidParse("t ; q = 0", new StringWithQualityHeaderValue("t", 0));
CheckValidParse("iso-8859-5", new StringWithQualityHeaderValue("iso-8859-5"));
CheckValidParse("unicode-1-1; q=0.8", new StringWithQualityHeaderValue("unicode-1-1", 0.8));
}
[Theory]
[InlineData("text,")]
[InlineData("\r\n text ; q = 0.5, next_text ")]
[InlineData(" text,next_text ")]
[InlineData(" ,, text, , ,next")]
[InlineData(" ,, text, , ,")]
[InlineData(", \r\n text \r\n ; \r\n q = 0.123")]
[InlineData("teäxt")]
[InlineData("text会")]
[InlineData("会")]
[InlineData("t;q=会")]
[InlineData("t;q=")]
[InlineData("t;q")]
[InlineData("t;会=1")]
[InlineData("t;q会=1")]
[InlineData("t y")]
[InlineData("t;q=1 y")]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData(" ,,")]
[InlineData("t;q=-1")]
[InlineData("t;q=1.00001")]
[InlineData("t;")]
[InlineData("t;;q=1")]
[InlineData("t;q=a")]
[InlineData("t;qa")]
[InlineData("t;q1")]
[InlineData("integer_part_too_long;q=01")]
[InlineData("integer_part_too_long;q=01.0")]
[InlineData("decimal_part_too_long;q=0.123456789")]
[InlineData("decimal_part_too_long;q=0.123456789 ")]
[InlineData("no_integer_part;q=.1")]
public void Parse_SetOfInvalidValueStrings_Throws(string input)
{
Assert.Throws<FormatException>(() => StringWithQualityHeaderValue.Parse(input));
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
CheckValidTryParse("text", new StringWithQualityHeaderValue("text"));
CheckValidTryParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5));
CheckValidTryParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5));
CheckValidTryParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5));
CheckValidTryParse(" text ", new StringWithQualityHeaderValue("text"));
CheckValidTryParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123));
}
[Fact]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
{
CheckInvalidTryParse("text,");
CheckInvalidTryParse("\r\n text ; q = 0.5, next_text ");
CheckInvalidTryParse(" text,next_text ");
CheckInvalidTryParse(" ,, text, , ,next");
CheckInvalidTryParse(" ,, text, , ,");
CheckInvalidTryParse(", \r\n text \r\n ; \r\n q = 0.123");
CheckInvalidTryParse("teäxt");
CheckInvalidTryParse("text会");
CheckInvalidTryParse("会");
CheckInvalidTryParse("t;q=会");
CheckInvalidTryParse("t;q=");
CheckInvalidTryParse("t;q");
CheckInvalidTryParse("t;会=1");
CheckInvalidTryParse("t;q会=1");
CheckInvalidTryParse("t y");
CheckInvalidTryParse("t;q=1 y");
CheckInvalidTryParse(null);
CheckInvalidTryParse(string.Empty);
CheckInvalidTryParse(" ");
CheckInvalidTryParse(" ,,");
}
[Fact]
public void ParseList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"text1",
"text2,",
"textA,textB",
"text3;q=0.5",
"text4;q=0.5,",
" text5 ; q = 0.50 ",
"\r\n text6 ; q = 0.05 ",
"text7,text8;q=0.5",
" text9 , text10 ; q = 0.5 ",
};
IList<StringWithQualityHeaderValue> results = StringWithQualityHeaderValue.ParseList(inputs);
var expectedResults = new[]
{
new StringWithQualityHeaderValue("text1"),
new StringWithQualityHeaderValue("text2"),
new StringWithQualityHeaderValue("textA"),
new StringWithQualityHeaderValue("textB"),
new StringWithQualityHeaderValue("text3", 0.5),
new StringWithQualityHeaderValue("text4", 0.5),
new StringWithQualityHeaderValue("text5", 0.5),
new StringWithQualityHeaderValue("text6", 0.05),
new StringWithQualityHeaderValue("text7"),
new StringWithQualityHeaderValue("text8", 0.5),
new StringWithQualityHeaderValue("text9"),
new StringWithQualityHeaderValue("text10", 0.5),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"text1",
"text2,",
"textA,textB",
"text3;q=0.5",
"text4;q=0.5,",
" text5 ; q = 0.50 ",
"\r\n text6 ; q = 0.05 ",
"text7,text8;q=0.5",
" text9 , text10 ; q = 0.5 ",
};
IList<StringWithQualityHeaderValue> results = StringWithQualityHeaderValue.ParseStrictList(inputs);
var expectedResults = new[]
{
new StringWithQualityHeaderValue("text1"),
new StringWithQualityHeaderValue("text2"),
new StringWithQualityHeaderValue("textA"),
new StringWithQualityHeaderValue("textB"),
new StringWithQualityHeaderValue("text3", 0.5),
new StringWithQualityHeaderValue("text4", 0.5),
new StringWithQualityHeaderValue("text5", 0.5),
new StringWithQualityHeaderValue("text6", 0.05),
new StringWithQualityHeaderValue("text7"),
new StringWithQualityHeaderValue("text8", 0.5),
new StringWithQualityHeaderValue("text9"),
new StringWithQualityHeaderValue("text10", 0.5),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"text1",
"text2,",
"textA,textB",
"text3;q=0.5",
"text4;q=0.5,",
" text5 ; q = 0.50 ",
"\r\n text6 ; q = 0.05 ",
"text7,text8;q=0.5",
" text9 , text10 ; q = 0.5 ",
};
IList<StringWithQualityHeaderValue> results;
Assert.True(StringWithQualityHeaderValue.TryParseList(inputs, out results));
var expectedResults = new[]
{
new StringWithQualityHeaderValue("text1"),
new StringWithQualityHeaderValue("text2"),
new StringWithQualityHeaderValue("textA"),
new StringWithQualityHeaderValue("textB"),
new StringWithQualityHeaderValue("text3", 0.5),
new StringWithQualityHeaderValue("text4", 0.5),
new StringWithQualityHeaderValue("text5", 0.5),
new StringWithQualityHeaderValue("text6", 0.05),
new StringWithQualityHeaderValue("text7"),
new StringWithQualityHeaderValue("text8", 0.5),
new StringWithQualityHeaderValue("text9"),
new StringWithQualityHeaderValue("text10", 0.5),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly()
{
var inputs = new[]
{
"",
"text1",
"text2,",
"textA,textB",
"text3;q=0.5",
"text4;q=0.5,",
" text5 ; q = 0.50 ",
"\r\n text6 ; q = 0.05 ",
"text7,text8;q=0.5",
" text9 , text10 ; q = 0.5 ",
};
IList<StringWithQualityHeaderValue> results;
Assert.True(StringWithQualityHeaderValue.TryParseStrictList(inputs, out results));
var expectedResults = new[]
{
new StringWithQualityHeaderValue("text1"),
new StringWithQualityHeaderValue("text2"),
new StringWithQualityHeaderValue("textA"),
new StringWithQualityHeaderValue("textB"),
new StringWithQualityHeaderValue("text3", 0.5),
new StringWithQualityHeaderValue("text4", 0.5),
new StringWithQualityHeaderValue("text5", 0.5),
new StringWithQualityHeaderValue("text6", 0.05),
new StringWithQualityHeaderValue("text7"),
new StringWithQualityHeaderValue("text8", 0.5),
new StringWithQualityHeaderValue("text9"),
new StringWithQualityHeaderValue("text10", 0.5),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseList_WithSomeInvlaidValues_IgnoresInvalidValues()
{
var inputs = new[]
{
"",
"text1",
"text 1",
"text2",
"\"text 2\",",
"text3;q=0.5",
"text4;q=0.5, extra stuff",
" text5 ; q = 0.50 ",
"\r\n text6 ; q = 0.05 ",
"text7,text8;q=0.5",
" text9 , text10 ; q = 0.5 ",
};
var results = StringWithQualityHeaderValue.ParseList(inputs);
var expectedResults = new[]
{
new StringWithQualityHeaderValue("text1"),
new StringWithQualityHeaderValue("1"),
new StringWithQualityHeaderValue("text2"),
new StringWithQualityHeaderValue("text3", 0.5),
new StringWithQualityHeaderValue("text4", 0.5),
new StringWithQualityHeaderValue("stuff"),
new StringWithQualityHeaderValue("text5", 0.5),
new StringWithQualityHeaderValue("text6", 0.05),
new StringWithQualityHeaderValue("text7"),
new StringWithQualityHeaderValue("text8", 0.5),
new StringWithQualityHeaderValue("text9"),
new StringWithQualityHeaderValue("text10", 0.5),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void ParseStrictList_WithSomeInvlaidValues_Throws()
{
var inputs = new[]
{
"",
"text1",
"text 1",
"text2",
"\"text 2\",",
"text3;q=0.5",
"text4;q=0.5, extra stuff",
" text5 ; q = 0.50 ",
"\r\n text6 ; q = 0.05 ",
"text7,text8;q=0.5",
" text9 , text10 ; q = 0.5 ",
};
Assert.Throws<FormatException>(() => StringWithQualityHeaderValue.ParseStrictList(inputs));
}
[Fact]
public void TryParseList_WithSomeInvlaidValues_IgnoresInvalidValues()
{
var inputs = new[]
{
"",
"text1",
"text 1",
"text2",
"\"text 2\",",
"text3;q=0.5",
"text4;q=0.5, extra stuff",
" text5 ; q = 0.50 ",
"\r\n text6 ; q = 0.05 ",
"text7,text8;q=0.5",
" text9 , text10 ; q = 0.5 ",
};
IList<StringWithQualityHeaderValue> results;
Assert.True(StringWithQualityHeaderValue.TryParseList(inputs, out results));
var expectedResults = new[]
{
new StringWithQualityHeaderValue("text1"),
new StringWithQualityHeaderValue("1"),
new StringWithQualityHeaderValue("text2"),
new StringWithQualityHeaderValue("text3", 0.5),
new StringWithQualityHeaderValue("text4", 0.5),
new StringWithQualityHeaderValue("stuff"),
new StringWithQualityHeaderValue("text5", 0.5),
new StringWithQualityHeaderValue("text6", 0.05),
new StringWithQualityHeaderValue("text7"),
new StringWithQualityHeaderValue("text8", 0.5),
new StringWithQualityHeaderValue("text9"),
new StringWithQualityHeaderValue("text10", 0.5),
}.ToList();
Assert.Equal(expectedResults, results);
}
[Fact]
public void TryParseStrictList_WithSomeInvlaidValues_ReturnsFalse()
{
var inputs = new[]
{
"",
"text1",
"text 1",
"text2",
"\"text 2\",",
"text3;q=0.5",
"text4;q=0.5, extra stuff",
" text5 ; q = 0.50 ",
"\r\n text6 ; q = 0.05 ",
"text7,text8;q=0.5",
" text9 , text10 ; q = 0.5 ",
};
IList<StringWithQualityHeaderValue> results;
Assert.False(StringWithQualityHeaderValue.TryParseStrictList(inputs, out results));
}
#region Helper methods
private void CheckValidParse(string input, StringWithQualityHeaderValue expectedResult)
{
var result = StringWithQualityHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckValidTryParse(string input, StringWithQualityHeaderValue expectedResult)
{
StringWithQualityHeaderValue result = null;
Assert.True(StringWithQualityHeaderValue.TryParse(input, out result));
Assert.Equal(expectedResult, result);
}
private void CheckInvalidTryParse(string input)
{
StringWithQualityHeaderValue result = null;
Assert.False(StringWithQualityHeaderValue.TryParse(input, out result));
Assert.Null(result);
}
#endregion
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Claims;
namespace Microsoft.AspNetCore.Http.Authentication
{
/// <summary>
/// Used to store the results of an Authenticate call.
/// </summary>
public class AuthenticateInfo
{
/// <summary>
/// The <see cref="ClaimsPrincipal"/>.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
/// <summary>
/// The <see cref="AuthenticationProperties"/>.
/// </summary>
public AuthenticationProperties Properties { get; set; }
/// <summary>
/// The <see cref="AuthenticationDescription"/>.
/// </summary>
public AuthenticationDescription Description { get; set; }
}
}

View File

@ -0,0 +1,68 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
namespace Microsoft.AspNetCore.Http.Authentication
{
/// <summary>
/// Contains information describing an authentication provider.
/// </summary>
public class AuthenticationDescription
{
private const string DisplayNamePropertyKey = "DisplayName";
private const string AuthenticationSchemePropertyKey = "AuthenticationScheme";
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationDescription"/> class
/// </summary>
public AuthenticationDescription()
: this(items: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationDescription"/> class
/// </summary>
/// <param name="items"></param>
public AuthenticationDescription(IDictionary<string, object> items)
{
Items = items ?? new Dictionary<string, object>(StringComparer.Ordinal); ;
}
/// <summary>
/// Contains metadata about the authentication provider.
/// </summary>
public IDictionary<string, object> Items { get; }
/// <summary>
/// Gets or sets the name used to reference the authentication middleware instance.
/// </summary>
public string AuthenticationScheme
{
get { return GetString(AuthenticationSchemePropertyKey); }
set { Items[AuthenticationSchemePropertyKey] = value; }
}
/// <summary>
/// Gets or sets the display name for the authentication provider.
/// </summary>
public string DisplayName
{
get { return GetString(DisplayNamePropertyKey); }
set { Items[DisplayNamePropertyKey] = value; }
}
private string GetString(string name)
{
object value;
if (Items.TryGetValue(name, out value))
{
return Convert.ToString(value, CultureInfo.InvariantCulture);
}
return null;
}
}
}

View File

@ -0,0 +1,132 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features.Authentication;
namespace Microsoft.AspNetCore.Http.Authentication
{
[Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")]
public abstract class AuthenticationManager
{
/// <summary>
/// Constant used to represent the automatic scheme
/// </summary>
public const string AutomaticScheme = "Automatic";
public abstract HttpContext HttpContext { get; }
public abstract IEnumerable<AuthenticationDescription> GetAuthenticationSchemes();
public abstract Task<AuthenticateInfo> GetAuthenticateInfoAsync(string authenticationScheme);
// Will remove once callees have been updated
public abstract Task AuthenticateAsync(AuthenticateContext context);
public virtual async Task<ClaimsPrincipal> AuthenticateAsync(string authenticationScheme)
{
return (await GetAuthenticateInfoAsync(authenticationScheme))?.Principal;
}
public virtual Task ChallengeAsync()
{
return ChallengeAsync(properties: null);
}
public virtual Task ChallengeAsync(AuthenticationProperties properties)
{
return ChallengeAsync(authenticationScheme: AutomaticScheme, properties: properties);
}
public virtual Task ChallengeAsync(string authenticationScheme)
{
if (string.IsNullOrEmpty(authenticationScheme))
{
throw new ArgumentException(nameof(authenticationScheme));
}
return ChallengeAsync(authenticationScheme: authenticationScheme, properties: null);
}
// Leave it up to authentication handler to do the right thing for the challenge
public virtual Task ChallengeAsync(string authenticationScheme, AuthenticationProperties properties)
{
if (string.IsNullOrEmpty(authenticationScheme))
{
throw new ArgumentException(nameof(authenticationScheme));
}
return ChallengeAsync(authenticationScheme, properties, ChallengeBehavior.Automatic);
}
public virtual Task SignInAsync(string authenticationScheme, ClaimsPrincipal principal)
{
if (string.IsNullOrEmpty(authenticationScheme))
{
throw new ArgumentException(nameof(authenticationScheme));
}
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
return SignInAsync(authenticationScheme, principal, properties: null);
}
/// <summary>
/// Creates a challenge for the authentication manager with <see cref="ChallengeBehavior.Forbidden"/>.
/// </summary>
/// <returns>A <see cref="Task"/> that represents the asynchronous challenge operation.</returns>
public virtual Task ForbidAsync()
=> ForbidAsync(AutomaticScheme, properties: null);
public virtual Task ForbidAsync(string authenticationScheme)
{
if (authenticationScheme == null)
{
throw new ArgumentNullException(nameof(authenticationScheme));
}
return ForbidAsync(authenticationScheme, properties: null);
}
// Deny access (typically a 403)
public virtual Task ForbidAsync(string authenticationScheme, AuthenticationProperties properties)
{
if (authenticationScheme == null)
{
throw new ArgumentNullException(nameof(authenticationScheme));
}
return ChallengeAsync(authenticationScheme, properties, ChallengeBehavior.Forbidden);
}
/// <summary>
/// Creates a challenge for the authentication manager with <see cref="ChallengeBehavior.Forbidden"/>.
/// </summary>
/// <param name="properties">Additional arbitrary values which may be used by particular authentication types.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous challenge operation.</returns>
public virtual Task ForbidAsync(AuthenticationProperties properties)
=> ForbidAsync(AutomaticScheme, properties);
public abstract Task ChallengeAsync(string authenticationScheme, AuthenticationProperties properties, ChallengeBehavior behavior);
public abstract Task SignInAsync(string authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties properties);
public virtual Task SignOutAsync(string authenticationScheme)
{
if (authenticationScheme == null)
{
throw new ArgumentNullException(nameof(authenticationScheme));
}
return SignOutAsync(authenticationScheme, properties: null);
}
public abstract Task SignOutAsync(string authenticationScheme, AuthenticationProperties properties);
}
}

View File

@ -0,0 +1,197 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
namespace Microsoft.AspNetCore.Http.Authentication
{
/// <summary>
/// Dictionary used to store state values about the authentication session.
/// </summary>
public class AuthenticationProperties
{
internal const string IssuedUtcKey = ".issued";
internal const string ExpiresUtcKey = ".expires";
internal const string IsPersistentKey = ".persistent";
internal const string RedirectUriKey = ".redirect";
internal const string RefreshKey = ".refresh";
internal const string UtcDateTimeFormat = "r";
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationProperties"/> class
/// </summary>
public AuthenticationProperties()
: this(items: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationProperties"/> class
/// </summary>
/// <param name="items"></param>
public AuthenticationProperties(IDictionary<string, string> items)
{
Items = items ?? new Dictionary<string, string>(StringComparer.Ordinal);
}
/// <summary>
/// State values about the authentication session.
/// </summary>
public IDictionary<string, string> Items { get; }
/// <summary>
/// Gets or sets whether the authentication session is persisted across multiple requests.
/// </summary>
public bool IsPersistent
{
get { return Items.ContainsKey(IsPersistentKey); }
set
{
if (Items.ContainsKey(IsPersistentKey))
{
if (!value)
{
Items.Remove(IsPersistentKey);
}
}
else
{
if (value)
{
Items.Add(IsPersistentKey, string.Empty);
}
}
}
}
/// <summary>
/// Gets or sets the full path or absolute URI to be used as an HTTP redirect response value.
/// </summary>
public string RedirectUri
{
get
{
string value;
return Items.TryGetValue(RedirectUriKey, out value) ? value : null;
}
set
{
if (value != null)
{
Items[RedirectUriKey] = value;
}
else
{
if (Items.ContainsKey(RedirectUriKey))
{
Items.Remove(RedirectUriKey);
}
}
}
}
/// <summary>
/// Gets or sets the time at which the authentication ticket was issued.
/// </summary>
public DateTimeOffset? IssuedUtc
{
get
{
string value;
if (Items.TryGetValue(IssuedUtcKey, out value))
{
DateTimeOffset dateTimeOffset;
if (DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dateTimeOffset))
{
return dateTimeOffset;
}
}
return null;
}
set
{
if (value.HasValue)
{
Items[IssuedUtcKey] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture);
}
else
{
if (Items.ContainsKey(IssuedUtcKey))
{
Items.Remove(IssuedUtcKey);
}
}
}
}
/// <summary>
/// Gets or sets the time at which the authentication ticket expires.
/// </summary>
public DateTimeOffset? ExpiresUtc
{
get
{
string value;
if (Items.TryGetValue(ExpiresUtcKey, out value))
{
DateTimeOffset dateTimeOffset;
if (DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dateTimeOffset))
{
return dateTimeOffset;
}
}
return null;
}
set
{
if (value.HasValue)
{
Items[ExpiresUtcKey] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture);
}
else
{
if (Items.ContainsKey(ExpiresUtcKey))
{
Items.Remove(ExpiresUtcKey);
}
}
}
}
/// <summary>
/// Gets or sets if refreshing the authentication session should be allowed.
/// </summary>
public bool? AllowRefresh
{
get
{
string value;
if (Items.TryGetValue(RefreshKey, out value))
{
bool refresh;
if (bool.TryParse(value, out refresh))
{
return refresh;
}
}
return null;
}
set
{
if (value.HasValue)
{
Items[RefreshKey] = value.Value.ToString();
}
else
{
if (Items.ContainsKey(RefreshKey))
{
Items.Remove(RefreshKey);
}
}
}
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http
{
public abstract class ConnectionInfo
{
/// <summary>
/// Gets or sets a unique identifier to represent this connection.
/// </summary>
public abstract string Id { get; set; }
public abstract IPAddress RemoteIpAddress { get; set; }
public abstract int RemotePort { get; set; }
public abstract IPAddress LocalIpAddress { get; set; }
public abstract int LocalPort { get; set; }
public abstract X509Certificate2 ClientCertificate { get; set; }
public abstract Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken = new CancellationToken());
}
}

View File

@ -0,0 +1,114 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http.Abstractions;
namespace Microsoft.AspNetCore.Http
{
/// <summary>
/// Defines settings used to create a cookie.
/// </summary>
public class CookieBuilder
{
private string _name;
/// <summary>
/// The name of the cookie.
/// </summary>
public virtual string Name
{
get => _name;
set => _name = !string.IsNullOrEmpty(value)
? value
: throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value));
}
/// <summary>
/// The cookie path.
/// </summary>
/// <remarks>
/// Determines the value that will set on <seealso cref="CookieOptions.Path"/>.
/// </remarks>
public virtual string Path { get; set; }
/// <summary>
/// The domain to associate the cookie with.
/// </summary>
/// <remarks>
/// Determines the value that will set on <seealso cref="CookieOptions.Domain"/>.
/// </remarks>
public virtual string Domain { get; set; }
/// <summary>
/// Indicates whether a cookie is accessible by client-side script.
/// </summary>
/// <remarks>
/// Determines the value that will set on <seealso cref="CookieOptions.HttpOnly"/>.
/// </remarks>
public virtual bool HttpOnly { get; set; }
/// <summary>
/// The SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Lax"/>
/// </summary>
/// <remarks>
/// Determines the value that will set on <seealso cref="CookieOptions.SameSite"/>.
/// </remarks>
public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.Lax;
/// <summary>
/// The policy that will be used to determine <seealso cref="CookieOptions.Secure"/>.
/// This is determined from the <see cref="HttpContext"/> passed to <see cref="Build(HttpContext, DateTimeOffset)"/>.
/// </summary>
public virtual CookieSecurePolicy SecurePolicy { get; set; }
/// <summary>
/// Gets or sets the lifespan of a cookie.
/// </summary>
public virtual TimeSpan? Expiration { get; set; }
/// <summary>
/// Gets or sets the max-age for the cookie.
/// </summary>
public virtual TimeSpan? MaxAge { get; set; }
/// <summary>
/// Indicates if this cookie is essential for the application to function correctly. If true then
/// consent policy checks may be bypassed. The default value is false.
/// </summary>
public virtual bool IsEssential { get; set; }
/// <summary>
/// Creates the cookie options from the given <paramref name="context"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <returns>The cookie options.</returns>
public CookieOptions Build(HttpContext context) => Build(context, DateTimeOffset.Now);
/// <summary>
/// Creates the cookie options from the given <paramref name="context"/> with an expiration based on <paramref name="expiresFrom"/> and <see cref="Expiration"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="expiresFrom">The time to use as the base for computing <seealso cref="CookieOptions.Expires" />.</param>
/// <returns>The cookie options.</returns>
public virtual CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new CookieOptions
{
Path = Path ?? "/",
SameSite = SameSite,
HttpOnly = HttpOnly,
MaxAge = MaxAge,
Domain = Domain,
IsEssential = IsEssential,
Secure = SecurePolicy == CookieSecurePolicy.Always || (SecurePolicy == CookieSecurePolicy.SameAsRequest && context.Request.IsHttps),
Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.Value) : default(DateTimeOffset?)
};
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Http
{
/// <summary>
/// Determines how cookie security properties are set.
/// </summary>
public enum CookieSecurePolicy
{
/// <summary>
/// If the URI that provides the cookie is HTTPS, then the cookie will only be returned to the server on
/// subsequent HTTPS requests. Otherwise if the URI that provides the cookie is HTTP, then the cookie will
/// be returned to the server on all HTTP and HTTPS requests. This is the default value because it ensures
/// HTTPS for all authenticated requests on deployed servers, and also supports HTTP for localhost development
/// and for servers that do not have HTTPS support.
/// </summary>
SameAsRequest,
/// <summary>
/// Secure is always marked true. Use this value when your login page and all subsequent pages
/// requiring the authenticated identity are HTTPS. Local development will also need to be done with HTTPS urls.
/// </summary>
Always,
/// <summary>
/// Secure is not marked true. Use this value when your login page is HTTPS, but other pages
/// on the site which are HTTP also require authentication information. This setting is not recommended because
/// the authentication information provided with an HTTP request may be observed and used by other computers
/// on your local network or wireless connection.
/// </summary>
None,
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Http
{
public static class HeaderDictionaryExtensions
{
/// <summary>
/// Add new values. Each item remains a separate array entry.
/// </summary>
/// <param name="headers">The <see cref="IHeaderDictionary"/> to use.</param>
/// <param name="key">The header name.</param>
/// <param name="value">The header value.</param>
public static void Append(this IHeaderDictionary headers, string key, StringValues value)
{
ParsingHelpers.AppendHeaderUnmodified(headers, key, value);
}
/// <summary>
/// Quotes any values containing commas, and then comma joins all of the values with any existing values.
/// </summary>
/// <param name="headers">The <see cref="IHeaderDictionary"/> to use.</param>
/// <param name="key">The header name.</param>
/// <param name="values">The header values.</param>
public static void AppendCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values)
{
ParsingHelpers.AppendHeaderJoined(headers, key, values);
}
/// <summary>
/// Get the associated values from the collection separated into individual values.
/// Quoted values will not be split, and the quotes will be removed.
/// </summary>
/// <param name="headers">The <see cref="IHeaderDictionary"/> to use.</param>
/// <param name="key">The header name.</param>
/// <returns>the associated values from the collection separated into individual values, or StringValues.Empty if the key is not present.</returns>
public static string[] GetCommaSeparatedValues(this IHeaderDictionary headers, string key)
{
return ParsingHelpers.GetHeaderSplit(headers, key).ToArray();
}
/// <summary>
/// Quotes any values containing commas, and then comma joins all of the values.
/// </summary>
/// <param name="headers">The <see cref="IHeaderDictionary"/> to use.</param>
/// <param name="key">The header name.</param>
/// <param name="values">The header values.</param>
public static void SetCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values)
{
ParsingHelpers.SetHeaderJoined(headers, key, values);
}
}
}

View File

@ -0,0 +1,67 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http
{
/// <summary>
/// Convenience methods for writing to the response.
/// </summary>
public static class HttpResponseWritingExtensions
{
/// <summary>
/// Writes the given text to the response body. UTF-8 encoding will be used.
/// </summary>
/// <param name="response">The <see cref="HttpResponse"/>.</param>
/// <param name="text">The text to write to the response.</param>
/// <param name="cancellationToken">Notifies when request operations should be cancelled.</param>
/// <returns>A task that represents the completion of the write operation.</returns>
public static Task WriteAsync(this HttpResponse response, string text, CancellationToken cancellationToken = default(CancellationToken))
{
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
return response.WriteAsync(text, Encoding.UTF8, cancellationToken);
}
/// <summary>
/// Writes the given text to the response body using the given encoding.
/// </summary>
/// <param name="response">The <see cref="HttpResponse"/>.</param>
/// <param name="text">The text to write to the response.</param>
/// <param name="encoding">The encoding to use.</param>
/// <param name="cancellationToken">Notifies when request operations should be cancelled.</param>
/// <returns>A task that represents the completion of the write operation.</returns>
public static Task WriteAsync(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken))
{
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
if (encoding == null)
{
throw new ArgumentNullException(nameof(encoding));
}
byte[] data = encoding.GetBytes(text);
return response.Body.WriteAsync(data, 0, data.Length, cancellationToken);
}
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Builder.Extensions;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods for the <see cref="MapMiddleware"/>.
/// </summary>
public static class MapExtensions
{
/// <summary>
/// Branches the request pipeline based on matches of the given request path. If the request path starts with
/// the given path, the branch is executed.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="pathMatch">The request path to match.</param>
/// <param name="configuration">The branch to take for positive path matches.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
if (pathMatch.HasValue && pathMatch.Value.EndsWith("/", StringComparison.Ordinal))
{
throw new ArgumentException("The path must not end with a '/'", nameof(pathMatch));
}
// create branch
var branchBuilder = app.New();
configuration(branchBuilder);
var branch = branchBuilder.Build();
var options = new MapOptions
{
Branch = branch,
PathMatch = pathMatch,
};
return app.Use(next => new MapMiddleware(next, options).Invoke);
}
}
}

View File

@ -0,0 +1,78 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Builder.Extensions
{
/// <summary>
/// Respresents a middleware that maps a request path to a sub-request pipeline.
/// </summary>
public class MapMiddleware
{
private readonly RequestDelegate _next;
private readonly MapOptions _options;
/// <summary>
/// Creates a new instace of <see cref="MapMiddleware"/>.
/// </summary>
/// <param name="next">The delegate representing the next middleware in the request pipeline.</param>
/// <param name="options">The middleware options.</param>
public MapMiddleware(RequestDelegate next, MapOptions options)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_next = next;
_options = options;
}
/// <summary>
/// Executes the middleware.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
/// <returns>A task that represents the execution of this middleware.</returns>
public async Task Invoke(HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
PathString matchedPath;
PathString remainingPath;
if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matchedPath, out remainingPath))
{
// Update the path
var path = context.Request.Path;
var pathBase = context.Request.PathBase;
context.Request.PathBase = pathBase.Add(matchedPath);
context.Request.Path = remainingPath;
try
{
await _options.Branch(context);
}
finally
{
context.Request.PathBase = pathBase;
context.Request.Path = path;
}
}
else
{
await _next(context);
}
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Builder.Extensions
{
/// <summary>
/// Options for the <see cref="MapMiddleware"/>.
/// </summary>
public class MapOptions
{
/// <summary>
/// The path to match.
/// </summary>
public PathString PathMatch { get; set; }
/// <summary>
/// The branch taken for a positive match.
/// </summary>
public RequestDelegate Branch { get; set; }
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Builder.Extensions;
namespace Microsoft.AspNetCore.Builder
{
using Predicate = Func<HttpContext, bool>;
/// <summary>
/// Extension methods for the <see cref="MapWhenMiddleware"/>.
/// </summary>
public static class MapWhenExtensions
{
/// <summary>
/// Branches the request pipeline based on the result of the given predicate.
/// </summary>
/// <param name="app"></param>
/// <param name="predicate">Invoked with the request environment to determine if the branch should be taken</param>
/// <param name="configuration">Configures a branch to take</param>
/// <returns></returns>
public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Predicate predicate, Action<IApplicationBuilder> configuration)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (predicate == null)
{
throw new ArgumentNullException(nameof(predicate));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
// create branch
var branchBuilder = app.New();
configuration(branchBuilder);
var branch = branchBuilder.Build();
// put middleware in pipeline
var options = new MapWhenOptions
{
Predicate = predicate,
Branch = branch,
};
return app.Use(next => new MapWhenMiddleware(next, options).Invoke);
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Builder.Extensions
{
/// <summary>
/// Respresents a middleware that runs a sub-request pipeline when a given predicate is matched.
/// </summary>
public class MapWhenMiddleware
{
private readonly RequestDelegate _next;
private readonly MapWhenOptions _options;
/// <summary>
/// Creates a new instance of <see cref="MapWhenMiddleware"/>.
/// </summary>
/// <param name="next">The delegate representing the next middleware in the request pipeline.</param>
/// <param name="options">The middleware options.</param>
public MapWhenMiddleware(RequestDelegate next, MapWhenOptions options)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_next = next;
_options = options;
}
/// <summary>
/// Executes the middleware.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
/// <returns>A task that represents the execution of this middleware.</returns>
public async Task Invoke(HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (_options.Predicate(context))
{
await _options.Branch(context);
}
else
{
await _next(context);
}
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Builder.Extensions
{
/// <summary>
/// Options for the <see cref="MapWhenMiddleware"/>.
/// </summary>
public class MapWhenOptions
{
private Func<HttpContext, bool> _predicate;
/// <summary>
/// The user callback that determines if the branch should be taken.
/// </summary>
public Func<HttpContext, bool> Predicate
{
get
{
return _predicate;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_predicate = value;
}
}
/// <summary>
/// The branch taken for a positive match.
/// </summary>
public RequestDelegate Branch { get; set; }
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods for adding terminal middleware.
/// </summary>
public static class RunExtensions
{
/// <summary>
/// Adds a terminal middleware delegate to the application's request pipeline.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="handler">A delegate that handles the request.</param>
public static void Run(this IApplicationBuilder app, RequestDelegate handler)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
app.Use(_ => handler);
}
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods for adding middleware.
/// </summary>
public static class UseExtensions
{
/// <summary>
/// Adds a middleware delegate defined in-line to the application's request pipeline.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="middleware">A function that handles the request or calls the given next function.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
{
return app.Use(next =>
{
return context =>
{
Func<Task> simpleNext = () => next(context);
return middleware(context, simpleNext);
};
});
}
}
}

View File

@ -0,0 +1,224 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Abstractions;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods for adding typed middleware.
/// </summary>
public static class UseMiddlewareExtensions
{
internal const string InvokeMethodName = "Invoke";
internal const string InvokeAsyncMethodName = "InvokeAsync";
private static readonly MethodInfo GetServiceInfo = typeof(UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static);
/// <summary>
/// Adds a middleware type to the application's request pipeline.
/// </summary>
/// <typeparam name="TMiddleware">The middleware type.</typeparam>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="args">The arguments to pass to the middleware type instance's constructor.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args)
{
return app.UseMiddleware(typeof(TMiddleware), args);
}
/// <summary>
/// Adds a middleware type to the application's request pipeline.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="middleware">The middleware type.</param>
/// <param name="args">The arguments to pass to the middleware type instance's constructor.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
{
if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
{
// IMiddleware doesn't support passing args directly since it's
// activated from the container
if (args.Length > 0)
{
throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
}
return UseMiddlewareInterface(app, middleware);
}
var applicationServices = app.ApplicationServices;
return app.Use(next =>
{
var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
var invokeMethods = methods.Where(m =>
string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)
|| string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)
).ToArray();
if (invokeMethods.Length > 1)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
}
if (invokeMethods.Length == 0)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
}
var methodinfo = invokeMethods[0];
if (!typeof(Task).IsAssignableFrom(methodinfo.ReturnType))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
}
var parameters = methodinfo.GetParameters();
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
}
var ctorArgs = new object[args.Length + 1];
ctorArgs[0] = next;
Array.Copy(args, 0, ctorArgs, 1, args.Length);
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
if (parameters.Length == 1)
{
return (RequestDelegate)methodinfo.CreateDelegate(typeof(RequestDelegate), instance);
}
var factory = Compile<object>(methodinfo, parameters);
return context =>
{
var serviceProvider = context.RequestServices ?? applicationServices;
if (serviceProvider == null)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
}
return factory(instance, context, serviceProvider);
};
});
}
private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, Type middlewareType)
{
return app.Use(next =>
{
return async context =>
{
var middlewareFactory = (IMiddlewareFactory)context.RequestServices.GetService(typeof(IMiddlewareFactory));
if (middlewareFactory == null)
{
// No middleware factory
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)));
}
var middleware = middlewareFactory.Create(middlewareType);
if (middleware == null)
{
// The factory returned null, it's a broken implementation
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType));
}
try
{
await middleware.InvokeAsync(context, next);
}
finally
{
middlewareFactory.Release(middleware);
}
};
});
}
private static Func<T, HttpContext, IServiceProvider, Task> Compile<T>(MethodInfo methodinfo, ParameterInfo[] parameters)
{
// If we call something like
//
// public class Middleware
// {
// public Task Invoke(HttpContext context, ILoggerFactory loggeryFactory)
// {
//
// }
// }
//
// We'll end up with something like this:
// Generic version:
//
// Task Invoke(Middleware instance, HttpContext httpContext, IServiceprovider provider)
// {
// return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory));
// }
// Non generic version:
//
// Task Invoke(object instance, HttpContext httpContext, IServiceprovider provider)
// {
// return ((Middleware)instance).Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory));
// }
var middleware = typeof(T);
var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext");
var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider");
var instanceArg = Expression.Parameter(middleware, "middleware");
var methodArguments = new Expression[parameters.Length];
methodArguments[0] = httpContextArg;
for (int i = 1; i < parameters.Length; i++)
{
var parameterType = parameters[i].ParameterType;
if (parameterType.IsByRef)
{
throw new NotSupportedException(Resources.FormatException_InvokeDoesNotSupportRefOrOutParams(InvokeMethodName));
}
var parameterTypeExpression = new Expression[]
{
providerArg,
Expression.Constant(parameterType, typeof(Type)),
Expression.Constant(methodinfo.DeclaringType, typeof(Type))
};
var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression);
methodArguments[i] = Expression.Convert(getServiceCall, parameterType);
}
Expression middlewareInstanceArg = instanceArg;
if (methodinfo.DeclaringType != typeof(T))
{
middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodinfo.DeclaringType);
}
var body = Expression.Call(middlewareInstanceArg, methodinfo, methodArguments);
var lambda = Expression.Lambda<Func<T, HttpContext, IServiceProvider, Task>>(body, instanceArg, httpContextArg, providerArg);
return lambda.Compile();
}
private static object GetService(IServiceProvider sp, Type type, Type middleware)
{
var service = sp.GetService(type);
if (service == null)
{
throw new InvalidOperationException(Resources.FormatException_InvokeMiddlewareNoService(type, middleware));
}
return service;
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Builder.Extensions;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods for <see cref="IApplicationBuilder"/>.
/// </summary>
public static class UsePathBaseExtensions
{
/// <summary>
/// Adds a middleware that extracts the specified path base from request path and postpend it to the request path base.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="pathBase">The path base to extract.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder UsePathBase(this IApplicationBuilder app, PathString pathBase)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
// Strip trailing slashes
pathBase = pathBase.Value?.TrimEnd('/');
if (!pathBase.HasValue)
{
return app;
}
return app.UseMiddleware<UsePathBaseMiddleware>(pathBase);
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Builder.Extensions
{
/// <summary>
/// Represents a middleware that extracts the specified path base from request path and postpend it to the request path base.
/// </summary>
public class UsePathBaseMiddleware
{
private readonly RequestDelegate _next;
private readonly PathString _pathBase;
/// <summary>
/// Creates a new instace of <see cref="UsePathBaseMiddleware"/>.
/// </summary>
/// <param name="next">The delegate representing the next middleware in the request pipeline.</param>
/// <param name="pathBase">The path base to extract.</param>
public UsePathBaseMiddleware(RequestDelegate next, PathString pathBase)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (!pathBase.HasValue)
{
throw new ArgumentException($"{nameof(pathBase)} cannot be null or empty.");
}
_next = next;
_pathBase = pathBase;
}
/// <summary>
/// Executes the middleware.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
/// <returns>A task that represents the execution of this middleware.</returns>
public async Task Invoke(HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
PathString matchedPath;
PathString remainingPath;
if (context.Request.Path.StartsWithSegments(_pathBase, out matchedPath, out remainingPath))
{
var originalPath = context.Request.Path;
var originalPathBase = context.Request.PathBase;
context.Request.Path = remainingPath;
context.Request.PathBase = originalPathBase.Add(matchedPath);
try
{
await _next(context);
}
finally
{
context.Request.Path = originalPath;
context.Request.PathBase = originalPathBase;
}
}
else
{
await _next(context);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More