Merge source code from aspnet/HttpAbstractions into this repo
This commit is contained in:
commit
98190bdaf9
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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-ä.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ä.html""" } }, // 'attachment', specifying a filename of foo-ä.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-ä-€.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-ä-€.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-ä.html", // "'attachment', specifying a filename of foo-ä.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-ä-€.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}"); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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?)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue