diff --git a/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs b/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs new file mode 100644 index 0000000000..5982143bcb --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs @@ -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 +{ + /// + /// Contains the result of an Authenticate call + /// + public class AuthenticateResult + { + protected AuthenticateResult() { } + + /// + /// If a ticket was produced, authenticate was successful. + /// + public bool Succeeded => Ticket != null; + + /// + /// The authentication ticket. + /// + public AuthenticationTicket Ticket { get; protected set; } + + /// + /// Gets the claims-principal with authenticated user identities. + /// + public ClaimsPrincipal Principal => Ticket?.Principal; + + /// + /// Additional state values for the authentication session. + /// + public AuthenticationProperties Properties { get; protected set; } + + /// + /// Holds failure information from the authentication. + /// + public Exception Failure { get; protected set; } + + /// + /// Indicates that there was no information returned for this authentication scheme. + /// + public bool None { get; protected set; } + + /// + /// Indicates that authentication was successful. + /// + /// The ticket representing the authentication result. + /// The result. + public static AuthenticateResult Success(AuthenticationTicket ticket) + { + if (ticket == null) + { + throw new ArgumentNullException(nameof(ticket)); + } + return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties }; + } + + /// + /// Indicates that there was no information returned for this authentication scheme. + /// + /// The result. + public static AuthenticateResult NoResult() + { + return new AuthenticateResult() { None = true }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure exception. + /// The result. + public static AuthenticateResult Fail(Exception failure) + { + return new AuthenticateResult() { Failure = failure }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure exception. + /// Additional state values for the authentication session. + /// The result. + public static AuthenticateResult Fail(Exception failure, AuthenticationProperties properties) + { + return new AuthenticateResult() { Failure = failure, Properties = properties }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure message. + /// The result. + public static AuthenticateResult Fail(string failureMessage) + => Fail(new Exception(failureMessage)); + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure message. + /// Additional state values for the authentication session. + /// The result. + public static AuthenticateResult Fail(string failureMessage, AuthenticationProperties properties) + => Fail(new Exception(failureMessage), properties); + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs b/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs new file mode 100644 index 0000000000..bb50c6534f --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs @@ -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 +{ + /// + /// Extension methods to expose Authentication on HttpContext. + /// + public static class AuthenticationHttpContextExtensions + { + /// + /// Extension method for authenticate using the scheme. + /// + /// The context. + /// The . + public static Task AuthenticateAsync(this HttpContext context) => + context.AuthenticateAsync(scheme: null); + + /// + /// Extension method for authenticate. + /// + /// The context. + /// The name of the authentication scheme. + /// The . + public static Task AuthenticateAsync(this HttpContext context, string scheme) => + context.RequestServices.GetRequiredService().AuthenticateAsync(context, scheme); + + /// + /// Extension method for Challenge. + /// + /// The context. + /// The name of the authentication scheme. + /// The result. + public static Task ChallengeAsync(this HttpContext context, string scheme) => + context.ChallengeAsync(scheme, properties: null); + + /// + /// Extension method for authenticate using the scheme. + /// + /// The context. + /// The task. + public static Task ChallengeAsync(this HttpContext context) => + context.ChallengeAsync(scheme: null, properties: null); + + /// + /// Extension method for authenticate using the scheme. + /// + /// The context. + /// The properties. + /// The task. + public static Task ChallengeAsync(this HttpContext context, AuthenticationProperties properties) => + context.ChallengeAsync(scheme: null, properties: properties); + + /// + /// Extension method for Challenge. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// The task. + public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService().ChallengeAsync(context, scheme, properties); + + /// + /// Extension method for Forbid. + /// + /// The context. + /// The name of the authentication scheme. + /// The task. + public static Task ForbidAsync(this HttpContext context, string scheme) => + context.ForbidAsync(scheme, properties: null); + + /// + /// Extension method for Forbid using the scheme.. + /// + /// The context. + /// The task. + public static Task ForbidAsync(this HttpContext context) => + context.ForbidAsync(scheme: null, properties: null); + + /// + /// Extension method for Forbid. + /// + /// The context. + /// The properties. + /// The task. + public static Task ForbidAsync(this HttpContext context, AuthenticationProperties properties) => + context.ForbidAsync(scheme: null, properties: properties); + + /// + /// Extension method for Forbid. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// The task. + public static Task ForbidAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService().ForbidAsync(context, scheme, properties); + + /// + /// Extension method for SignIn. + /// + /// The context. + /// The name of the authentication scheme. + /// The user. + /// The task. + public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal) => + context.SignInAsync(scheme, principal, properties: null); + + /// + /// Extension method for SignIn using the . + /// + /// The context. + /// The user. + /// The task. + public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal) => + context.SignInAsync(scheme: null, principal: principal, properties: null); + + /// + /// Extension method for SignIn using the . + /// + /// The context. + /// The user. + /// The properties. + /// The task. + public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal, AuthenticationProperties properties) => + context.SignInAsync(scheme: null, principal: principal, properties: properties); + + /// + /// Extension method for SignIn. + /// + /// The context. + /// The name of the authentication scheme. + /// The user. + /// The properties. + /// The task. + public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService().SignInAsync(context, scheme, principal, properties); + + /// + /// Extension method for SignOut using the . + /// + /// The context. + /// The task. + public static Task SignOutAsync(this HttpContext context) => context.SignOutAsync(scheme: null, properties: null); + + /// + /// Extension method for SignOut using the . + /// + /// The context. + /// The properties. + /// The task. + public static Task SignOutAsync(this HttpContext context, AuthenticationProperties properties) => context.SignOutAsync(scheme: null, properties: properties); + + /// + /// Extension method for SignOut. + /// + /// The context. + /// The name of the authentication scheme. + /// The task. + public static Task SignOutAsync(this HttpContext context, string scheme) => context.SignOutAsync(scheme, properties: null); + + /// + /// Extension method for SignOut. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// + public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService().SignOutAsync(context, scheme, properties); + + /// + /// Extension method for getting the value of an authentication token. + /// + /// The context. + /// The name of the authentication scheme. + /// The name of the token. + /// The value of the token. + public static Task GetTokenAsync(this HttpContext context, string scheme, string tokenName) => + context.RequestServices.GetRequiredService().GetTokenAsync(context, scheme, tokenName); + + /// + /// Extension method for getting the value of an authentication token. + /// + /// The context. + /// The name of the token. + /// The value of the token. + public static Task GetTokenAsync(this HttpContext context, string tokenName) => + context.RequestServices.GetRequiredService().GetTokenAsync(context, tokenName); + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs b/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs new file mode 100644 index 0000000000..2781a35757 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs @@ -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 _schemes = new List(); + + /// + /// Returns the schemes in the order they were added (important for request handling priority) + /// + public IEnumerable Schemes => _schemes; + + /// + /// Maps schemes by name. + /// + public IDictionary SchemeMap { get; } = new Dictionary(StringComparer.Ordinal); + + /// + /// Adds an . + /// + /// The name of the scheme being added. + /// Configures the scheme. + public void AddScheme(string name, Action 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; + } + + /// + /// Adds an . + /// + /// The responsible for the scheme. + /// The name of the scheme being added. + /// The display name for the scheme. + public void AddScheme(string name, string displayName) where THandler : IAuthenticationHandler + => AddScheme(name, b => + { + b.DisplayName = displayName; + b.HandlerType = typeof(THandler); + }); + + /// + /// Used as the fallback default scheme for all the other defaults. + /// + public string DefaultScheme { get; set; } + + /// + /// Used as the default scheme by . + /// + public string DefaultAuthenticateScheme { get; set; } + + /// + /// Used as the default scheme by . + /// + public string DefaultSignInScheme { get; set; } + + /// + /// Used as the default scheme by . + /// + public string DefaultSignOutScheme { get; set; } + + /// + /// Used as the default scheme by . + /// + public string DefaultChallengeScheme { get; set; } + + /// + /// Used as the default scheme by . + /// + public string DefaultForbidScheme { get; set; } + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs new file mode 100644 index 0000000000..271329209a --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs @@ -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 +{ + /// + /// Dictionary used to store state values about the authentication session. + /// + 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"; + + /// + /// Initializes a new instance of the class. + /// + public AuthenticationProperties() + : this(items: null, parameters: null) + { } + + /// + /// Initializes a new instance of the class. + /// + /// State values dictionary to use. + public AuthenticationProperties(IDictionary items) + : this(items, parameters: null) + { } + + /// + /// Initializes a new instance of the class. + /// + /// State values dictionary to use. + /// Parameters dictionary to use. + public AuthenticationProperties(IDictionary items, IDictionary parameters) + { + Items = items ?? new Dictionary(StringComparer.Ordinal); + Parameters = parameters ?? new Dictionary(StringComparer.Ordinal); + } + + /// + /// State values about the authentication session. + /// + public IDictionary Items { get; } + + /// + /// 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. + /// + public IDictionary Parameters { get; } + + /// + /// Gets or sets whether the authentication session is persisted across multiple requests. + /// + public bool IsPersistent + { + get => GetString(IsPersistentKey) != null; + set => SetString(IsPersistentKey, value ? string.Empty : null); + } + + /// + /// Gets or sets the full path or absolute URI to be used as an http redirect response value. + /// + public string RedirectUri + { + get => GetString(RedirectUriKey); + set => SetString(RedirectUriKey, value); + } + + /// + /// Gets or sets the time at which the authentication ticket was issued. + /// + public DateTimeOffset? IssuedUtc + { + get => GetDateTimeOffset(IssuedUtcKey); + set => SetDateTimeOffset(IssuedUtcKey, value); + } + + /// + /// Gets or sets the time at which the authentication ticket expires. + /// + public DateTimeOffset? ExpiresUtc + { + get => GetDateTimeOffset(ExpiresUtcKey); + set => SetDateTimeOffset(ExpiresUtcKey, value); + } + + /// + /// Gets or sets if refreshing the authentication session should be allowed. + /// + public bool? AllowRefresh + { + get => GetBool(RefreshKey); + set => SetBool(RefreshKey, value); + } + + /// + /// Get a string value from the collection. + /// + /// Property key. + /// Retrieved value or null if the property is not set. + public string GetString(string key) + { + return Items.TryGetValue(key, out string value) ? value : null; + } + + /// + /// Set a string value in the collection. + /// + /// Property key. + /// Value to set or null to remove the property. + public void SetString(string key, string value) + { + if (value != null) + { + Items[key] = value; + } + else if (Items.ContainsKey(key)) + { + Items.Remove(key); + } + } + + /// + /// Get a parameter from the collection. + /// + /// Parameter type. + /// Parameter key. + /// Retrieved value or the default value if the property is not set. + public T GetParameter(string key) + => Parameters.TryGetValue(key, out var obj) && obj is T value ? value : default; + + /// + /// Set a parameter value in the collection. + /// + /// Parameter type. + /// Parameter key. + /// Value to set. + public void SetParameter(string key, T value) + => Parameters[key] = value; + + /// + /// Get a bool value from the collection. + /// + /// Property key. + /// Retrieved value or null if the property is not set. + protected bool? GetBool(string key) + { + if (Items.TryGetValue(key, out string value) && bool.TryParse(value, out bool boolValue)) + { + return boolValue; + } + return null; + } + + /// + /// Set a bool value in the collection. + /// + /// Property key. + /// Value to set or null to remove the property. + protected void SetBool(string key, bool? value) + { + if (value.HasValue) + { + Items[key] = value.Value.ToString(); + } + else if (Items.ContainsKey(key)) + { + Items.Remove(key); + } + } + + /// + /// Get a DateTimeOffset value from the collection. + /// + /// Property key. + /// Retrieved value or null if the property is not set. + 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; + } + + /// + /// Set a DateTimeOffset value in the collection. + /// + /// Property key. + /// Value to set or null to remove the property. + 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); + } + } + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs b/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs new file mode 100644 index 0000000000..a72dc893ed --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs @@ -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 +{ + /// + /// AuthenticationSchemes assign a name to a specific + /// handlerType. + /// + public class AuthenticationScheme + { + /// + /// Constructor. + /// + /// The name for the authentication scheme. + /// The display name for the authentication scheme. + /// The type that handles this scheme. + 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; + } + + /// + /// The name of the authentication scheme. + /// + public string Name { get; } + + /// + /// The display name for the scheme. Null is valid and used for non user facing schemes. + /// + public string DisplayName { get; } + + /// + /// The type that handles this scheme. + /// + public Type HandlerType { get; } + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs b/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs new file mode 100644 index 0000000000..30e843c028 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs @@ -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 +{ + /// + /// Used to build s. + /// + public class AuthenticationSchemeBuilder + { + /// + /// Constructor. + /// + /// The name of the scheme being built. + public AuthenticationSchemeBuilder(string name) + { + Name = name; + } + + /// + /// The name of the scheme being built. + /// + public string Name { get; } + + /// + /// The display name for the scheme being built. + /// + public string DisplayName { get; set; } + + /// + /// The type responsible for this scheme. + /// + public Type HandlerType { get; set; } + + /// + /// Builds the instance. + /// + /// + public AuthenticationScheme Build() => new AuthenticationScheme(Name, DisplayName, HandlerType); + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs b/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs new file mode 100644 index 0000000000..c31f15ec01 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs @@ -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 +{ + /// + /// Contains user identity information as well as additional authentication state. + /// + public class AuthenticationTicket + { + /// + /// Initializes a new instance of the class + /// + /// the that represents the authenticated user. + /// additional properties that can be consumed by the user or runtime. + /// the authentication middleware that was responsible for this ticket. + public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties properties, string authenticationScheme) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + AuthenticationScheme = authenticationScheme; + Principal = principal; + Properties = properties ?? new AuthenticationProperties(); + } + + /// + /// Initializes a new instance of the class + /// + /// the that represents the authenticated user. + /// the authentication middleware that was responsible for this ticket. + public AuthenticationTicket(ClaimsPrincipal principal, string authenticationScheme) + : this(principal, properties: null, authenticationScheme: authenticationScheme) + { } + + /// + /// Gets the authentication type. + /// + public string AuthenticationScheme { get; private set; } + + /// + /// Gets the claims-principal with authenticated user identities. + /// + public ClaimsPrincipal Principal { get; private set; } + + /// + /// Additional state values for the authentication session. + /// + public AuthenticationProperties Properties { get; private set; } + } +} diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs new file mode 100644 index 0000000000..555da9e098 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs @@ -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 +{ + /// + /// Name/Value representing an token. + /// + public class AuthenticationToken + { + /// + /// Name. + /// + public string Name { get; set; } + + /// + /// Value. + /// + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs new file mode 100644 index 0000000000..43e5a13b49 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs @@ -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 +{ + /// + /// Used to capture path info so redirects can be computed properly within an app.Map(). + /// + public interface IAuthenticationFeature + { + /// + /// The original path base. + /// + PathString OriginalPathBase { get; set; } + + /// + /// The original path. + /// + PathString OriginalPath { get; set; } + } +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs new file mode 100644 index 0000000000..aeb373e18e --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs @@ -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 +{ + /// + /// Created per request to handle authentication for to a particular scheme. + /// + public interface IAuthenticationHandler + { + /// + /// The handler should initialize anything it needs from the request and scheme here. + /// + /// The scheme. + /// The context. + /// + Task InitializeAsync(AuthenticationScheme scheme, HttpContext context); + + /// + /// Authentication behavior. + /// + /// The result. + Task AuthenticateAsync(); + + /// + /// Challenge behavior. + /// + /// The that contains the extra meta-data arriving with the authentication. + /// A task. + Task ChallengeAsync(AuthenticationProperties properties); + + /// + /// Forbid behavior. + /// + /// The that contains the extra meta-data arriving with the authentication. + /// A task. + Task ForbidAsync(AuthenticationProperties properties); + } +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs new file mode 100644 index 0000000000..0507f51d61 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs @@ -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 +{ + /// + /// Provides the appropriate IAuthenticationHandler instance for the authenticationScheme and request. + /// + public interface IAuthenticationHandlerProvider + { + /// + /// Returns the handler instance that will be used. + /// + /// The context. + /// The name of the authentication scheme being handled. + /// The handler instance. + Task GetHandlerAsync(HttpContext context, string authenticationScheme); + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs new file mode 100644 index 0000000000..fb1b227ad7 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs @@ -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 +{ + /// + /// Used to determine if a handler wants to participate in request processing. + /// + public interface IAuthenticationRequestHandler : IAuthenticationHandler + { + /// + /// Returns true if request processing should stop. + /// + /// + Task HandleRequestAsync(); + } + +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs new file mode 100644 index 0000000000..3d2584fca8 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs @@ -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 +{ + /// + /// Responsible for managing what authenticationSchemes are supported. + /// + public interface IAuthenticationSchemeProvider + { + /// + /// Returns all currently registered s. + /// + /// All currently registered s. + Task> GetAllSchemesAsync(); + + /// + /// Returns the matching the name, or null. + /// + /// The name of the authenticationScheme. + /// The scheme or null if not found. + Task GetSchemeAsync(string name); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultAuthenticateSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultChallengeSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultForbidSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultSignInSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultSignOutSchemeAsync(); + + /// + /// Registers a scheme for use by . + /// + /// The scheme. + void AddScheme(AuthenticationScheme scheme); + + /// + /// Removes a scheme, preventing it from being used by . + /// + /// The name of the authenticationScheme being removed. + void RemoveScheme(string name); + + /// + /// Returns the schemes in priority order for request handling. + /// + /// The schemes in priority order for request handling + Task> GetRequestHandlerSchemesAsync(); + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs new file mode 100644 index 0000000000..e5d5336016 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs @@ -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 +{ + /// + /// Used to provide authentication. + /// + public interface IAuthenticationService + { + /// + /// Authenticate for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The result. + Task AuthenticateAsync(HttpContext context, string scheme); + + /// + /// Challenge the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties); + + /// + /// Forbids the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties); + + /// + /// Sign a principal in for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The to sign in. + /// The . + /// A task. + Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties); + + /// + /// Sign out the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties); + } +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs new file mode 100644 index 0000000000..69b88032d5 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs @@ -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 +{ + /// + /// Used to determine if a handler supports SignIn. + /// + public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler + { + /// + /// Handle sign in. + /// + /// The user. + /// The that contains the extra meta-data arriving with the authentication. + /// A task. + Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties); + } +} diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs new file mode 100644 index 0000000000..f76d116a76 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs @@ -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 +{ + /// + /// Used to determine if a handler supports SignOut. + /// + public interface IAuthenticationSignOutHandler : IAuthenticationHandler + { + /// + /// Signout behavior. + /// + /// The that contains the extra meta-data arriving with the authentication. + /// A task. + Task SignOutAsync(AuthenticationProperties properties); + } + +} diff --git a/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs b/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs new file mode 100644 index 0000000000..0193d95783 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs @@ -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 +{ + /// + /// Used by the for claims transformation. + /// + public interface IClaimsTransformation + { + /// + /// 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. + /// + /// The to transform. + /// The transformed principal. + Task TransformAsync(ClaimsPrincipal principal); + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/Microsoft.AspNetCore.Authentication.Abstractions.csproj b/src/Http/Authentication.Abstractions/src/Microsoft.AspNetCore.Authentication.Abstractions.csproj new file mode 100644 index 0000000000..bfb6e8e9ed --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/Microsoft.AspNetCore.Authentication.Abstractions.csproj @@ -0,0 +1,17 @@ + + + ASP.NET Core common types used by the various authentication components. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + false + + + + + + + + + \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/TokenExtensions.cs b/src/Http/Authentication.Abstractions/src/TokenExtensions.cs new file mode 100644 index 0000000000..497acabc23 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/TokenExtensions.cs @@ -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 +{ + /// + /// Extension methods for storing authentication tokens in . + /// + public static class AuthenticationTokenExtensions + { + private static string TokenNamesKey = ".TokenNames"; + private static string TokenKeyPrefix = ".Token."; + + /// + /// Stores a set of authentication tokens, after removing any old tokens. + /// + /// The properties. + /// The tokens to store. + public static void StoreTokens(this AuthenticationProperties properties, IEnumerable 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(); + 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()); + } + } + + /// + /// Returns the value of a token. + /// + /// The properties. + /// The token name. + /// The token value. + 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; + } + + /// + /// Returns all of the AuthenticationTokens contained in the properties. + /// + /// The properties. + /// The authentication toekns. + public static IEnumerable GetTokens(this AuthenticationProperties properties) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + + var tokens = new List(); + 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; + } + + /// + /// Extension method for getting the value of an authentication token. + /// + /// The . + /// The context. + /// The name of the token. + /// The value of the token. + public static Task GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName) + => auth.GetTokenAsync(context, scheme: null, tokenName: tokenName); + + /// + /// Extension method for getting the value of an authentication token. + /// + /// The . + /// The context. + /// The name of the authentication scheme. + /// The name of the token. + /// The value of the token. + public static async Task 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); + } + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Abstractions/src/baseline.netcore.json b/src/Http/Authentication.Abstractions/src/baseline.netcore.json new file mode 100644 index 0000000000..2d1e7e00e4 --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/baseline.netcore.json @@ -0,0 +1,1734 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Succeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Ticket", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationTicket", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Ticket", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Failure", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Failure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_None", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_None", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "NoResult", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "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", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "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", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "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", + "Static": true, + "Extension": true, + "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" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "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", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "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", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Schemes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SchemeMap", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "configureBuilder", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "THandler", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "get_DefaultScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultAuthenticateScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultAuthenticateScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultSignInScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultSignInScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultSignOutScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultSignOutScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultChallengeScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultChallengeScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultForbidScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultForbidScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsPersistent", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsPersistent", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RedirectUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RedirectUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IssuedUtc", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IssuedUtc", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpiresUtc", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ExpiresUtc", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowRefresh", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AllowRefresh", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationScheme", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HandlerType", + "Parameters": [], + "ReturnType": "System.Type", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "handlerType", + "Type": "System.Type" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DisplayName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HandlerType", + "Parameters": [], + "ReturnType": "System.Type", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HandlerType", + "Parameters": [ + { + "Name": "value", + "Type": "System.Type" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationScheme", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationTicket", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationToken", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Value", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OriginalPathBase", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OriginalPathBase", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OriginalPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OriginalPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "InitializeAsync", + "Parameters": [ + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetHandlerAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetAllSchemesAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task>", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetSchemeAsync", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultAuthenticateSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultChallengeSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultForbidSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultSignInSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultSignOutSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoveScheme", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetRequestHandlerSchemesAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationService", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "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", + "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", + "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", + "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", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IClaimsTransformation", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "TransformAsync", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationTokenExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "StoreTokens", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenValue", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UpdateTokenValue", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokenName", + "Type": "System.String" + }, + { + "Name": "tokenValue", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokens", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenAsync", + "Parameters": [ + { + "Name": "auth", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationService" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokenAsync", + "Parameters": [ + { + "Name": "auth", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationService" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "tokenName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs b/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs new file mode 100644 index 0000000000..fdf85a9b45 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs @@ -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 +{ + /// + /// Extension methods for setting up authentication services in an . + /// + public static class AuthenticationCoreServiceCollectionExtensions + { + /// + /// Add core authentication services needed for . + /// + /// The . + /// The service collection. + public static IServiceCollection AddAuthenticationCore(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddScoped(); + services.TryAddSingleton(); // Can be replaced with scoped ones that use DbContext + services.TryAddScoped(); + services.TryAddSingleton(); + return services; + } + + /// + /// Add core authentication services needed for . + /// + /// The . + /// Used to configure the . + /// The service collection. + public static IServiceCollection AddAuthenticationCore(this IServiceCollection services, Action configureOptions) { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.AddAuthenticationCore(); + services.Configure(configureOptions); + return services; + } + } +} diff --git a/src/Http/Authentication.Core/src/AuthenticationFeature.cs b/src/Http/Authentication.Core/src/AuthenticationFeature.cs new file mode 100644 index 0000000000..3282cbf467 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationFeature.cs @@ -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 +{ + /// + /// Used to capture path info so redirects can be computed properly within an app.Map(). + /// + public class AuthenticationFeature : IAuthenticationFeature + { + /// + /// The original path base. + /// + public PathString OriginalPathBase { get; set; } + + /// + /// The original path. + /// + public PathString OriginalPath { get; set; } + } +} diff --git a/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs b/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs new file mode 100644 index 0000000000..c4921e5334 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs @@ -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 +{ + /// + /// Implementation of . + /// + public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider + { + /// + /// Constructor. + /// + /// The . + public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes) + { + Schemes = schemes; + } + + /// + /// The . + /// + public IAuthenticationSchemeProvider Schemes { get; } + + // handler instance cache, need to initialize once per request + private Dictionary _handlerMap = new Dictionary(StringComparer.Ordinal); + + /// + /// Returns the handler instance that will be used. + /// + /// The context. + /// The name of the authentication scheme being handled. + /// The handler instance. + public async Task 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; + } + } +} diff --git a/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs b/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs new file mode 100644 index 0000000000..050118d3c4 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs @@ -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 +{ + /// + /// Implements . + /// + public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider + { + /// + /// Creates an instance of + /// using the specified , + /// + /// The options. + public AuthenticationSchemeProvider(IOptions options) + : this(options, new Dictionary(StringComparer.Ordinal)) + { + } + + /// + /// Creates an instance of + /// using the specified and . + /// + /// The options. + /// The dictionary used to store authentication schemes. + protected AuthenticationSchemeProvider(IOptions options, IDictionary schemes) + { + _options = options.Value; + + _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); + _requestHandlers = new List(); + + 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 _schemes; + private readonly List _requestHandlers; + + private Task GetDefaultSchemeAsync() + => _options.DefaultScheme != null + ? GetSchemeAsync(_options.DefaultScheme) + : Task.FromResult(null); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultAuthenticateSchemeAsync() + => _options.DefaultAuthenticateScheme != null + ? GetSchemeAsync(_options.DefaultAuthenticateScheme) + : GetDefaultSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultChallengeSchemeAsync() + => _options.DefaultChallengeScheme != null + ? GetSchemeAsync(_options.DefaultChallengeScheme) + : GetDefaultSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultForbidSchemeAsync() + => _options.DefaultForbidScheme != null + ? GetSchemeAsync(_options.DefaultForbidScheme) + : GetDefaultChallengeSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultSignInSchemeAsync() + => _options.DefaultSignInScheme != null + ? GetSchemeAsync(_options.DefaultSignInScheme) + : GetDefaultSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise this will fallback to if that supoorts sign out. + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultSignOutSchemeAsync() + => _options.DefaultSignOutScheme != null + ? GetSchemeAsync(_options.DefaultSignOutScheme) + : GetDefaultSignInSchemeAsync(); + + /// + /// Returns the matching the name, or null. + /// + /// The name of the authenticationScheme. + /// The scheme or null if not found. + public virtual Task GetSchemeAsync(string name) + => Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null); + + /// + /// Returns the schemes in priority order for request handling. + /// + /// The schemes in priority order for request handling + public virtual Task> GetRequestHandlerSchemesAsync() + => Task.FromResult>(_requestHandlers); + + /// + /// Registers a scheme for use by . + /// + /// The scheme. + 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; + } + } + + /// + /// Removes a scheme, preventing it from being used by . + /// + /// The name of the authenticationScheme being removed. + 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> GetAllSchemesAsync() + => Task.FromResult>(_schemes.Values); + } +} \ No newline at end of file diff --git a/src/Http/Authentication.Core/src/AuthenticationService.cs b/src/Http/Authentication.Core/src/AuthenticationService.cs new file mode 100644 index 0000000000..3e46df2f24 --- /dev/null +++ b/src/Http/Authentication.Core/src/AuthenticationService.cs @@ -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 +{ + /// + /// Implements . + /// + public class AuthenticationService : IAuthenticationService + { + /// + /// Constructor. + /// + /// The . + /// The . + /// The . + public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform) + { + Schemes = schemes; + Handlers = handlers; + Transform = transform; + } + + /// + /// Used to lookup AuthenticationSchemes. + /// + public IAuthenticationSchemeProvider Schemes { get; } + + /// + /// Used to resolve IAuthenticationHandler instances. + /// + public IAuthenticationHandlerProvider Handlers { get; } + + /// + /// Used for claims transformation. + /// + public IClaimsTransformation Transform { get; } + + /// + /// Authenticate for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The result. + public virtual async Task 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; + } + + /// + /// Challenge the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + 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); + } + + /// + /// Forbid the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + 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); + } + + /// + /// Sign a principal in for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The to sign in. + /// The . + /// A task. + 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); + } + + /// + /// Sign out the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + 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 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 GetAllSignInSchemeNames() + { + return string.Join(", ", (await Schemes.GetAllSchemesAsync()) + .Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType)) + .Select(sch => sch.Name)); + } + + private async Task 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 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 GetAllSignOutSchemeNames() + { + return string.Join(", ", (await Schemes.GetAllSchemesAsync()) + .Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType)) + .Select(sch => sch.Name)); + } + + private async Task 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 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}."); + } + } +} diff --git a/src/Http/Authentication.Core/src/Microsoft.AspNetCore.Authentication.Core.csproj b/src/Http/Authentication.Core/src/Microsoft.AspNetCore.Authentication.Core.csproj new file mode 100644 index 0000000000..c10bfb3656 --- /dev/null +++ b/src/Http/Authentication.Core/src/Microsoft.AspNetCore.Authentication.Core.csproj @@ -0,0 +1,18 @@ + + + + ASP.NET Core common types used by the various authentication middleware components. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + false + + + + + + + + + diff --git a/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs b/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs new file mode 100644 index 0000000000..83c488fe42 --- /dev/null +++ b/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs @@ -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 +{ + /// + /// Default claims transformation is a no-op. + /// + public class NoopClaimsTransformation : IClaimsTransformation + { + /// + /// Returns the principal unchanged. + /// + /// The user. + /// The principal unchanged. + public virtual Task TransformAsync(ClaimsPrincipal principal) + { + return Task.FromResult(principal); + } + } +} diff --git a/src/Http/Authentication.Core/src/baseline.netcore.json b/src/Http/Authentication.Core/src/baseline.netcore.json new file mode 100644 index 0000000000..62aeb44738 --- /dev/null +++ b/src/Http/Authentication.Core/src/baseline.netcore.json @@ -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", + "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", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultChallengeSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultForbidSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultSignInSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultSignOutSchemeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "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", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetRequestHandlerSchemesAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task>", + "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>", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "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", + "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", + "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" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs new file mode 100644 index 0000000000..639c9b558e --- /dev/null +++ b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs @@ -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 + { + ["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 + { + ["foo"] = "bar", + }; + var parameters = new Dictionary + { + ["number"] = 1234, + ["list"] = new List { "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("foo")); + Assert.Equal(0, props.Parameters.Count); + + props.SetParameter("foo", "foo bar"); + Assert.Equal("foo bar", props.GetParameter("foo")); + Assert.Equal("foo bar", props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter("foo", null); + Assert.Null(props.GetParameter("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("foo")); + Assert.Equal(0, props.Parameters.Count); + + props.SetParameter("foo", 123); + Assert.Equal(123, props.GetParameter("foo")); + Assert.Equal(123, props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter("foo", null); + Assert.Null(props.GetParameter("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("foo")); + Assert.Equal(0, props.Parameters.Count); + + var list = new string[] { "a", "b", "c" }; + props.SetParameter>("foo", list); + Assert.Equal(new string[] { "a", "b", "c" }, props.GetParameter>("foo")); + Assert.Same(list, props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter>("foo", null); + Assert.Null(props.GetParameter>("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); + } + } +} diff --git a/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs b/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs new file mode 100644 index 0000000000..82602000aa --- /dev/null +++ b/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs @@ -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("B", "whatever"); + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + 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("B", "whatever"); + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + 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("signin", "whatever"); + o.AddScheme("foobly", "whatever"); + o.DefaultSignInScheme = "signin"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + 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("challenge", "whatever"); + o.AddScheme("foobly", "whatever"); + o.DefaultChallengeScheme = "challenge"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + 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("A", "whatever"); + o.AddScheme("B", "whatever"); + o.AddScheme("C", "whatever"); + o.AddScheme("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(); + 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("signin", "whatever"); + o.DefaultSignInScheme = "signin"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + Assert.NotNull(await provider.GetDefaultSignOutSchemeAsync()); + } + + [Fact] + public void SchemeRegistrationIsCaseSensitive() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("signin", "whatever"); + o.AddScheme("signin", "whatever"); + }).BuildServiceProvider(); + + var error = Assert.Throws(() => services.GetRequiredService()); + + Assert.Contains("Scheme already exists: signin", error.Message); + } + + [Fact] + public async Task LookupUsesProvidedStringComparer() + { + var services = new ServiceCollection().AddOptions() + .AddSingleton() + .AddAuthenticationCore(o => o.AddScheme("signin", "whatever")) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + 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 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 options) + : base(options, new Dictionary(StringComparer.OrdinalIgnoreCase)) + { + } + } + } +} diff --git a/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs b/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs new file mode 100644 index 0000000000..e21ea40d51 --- /dev/null +++ b/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs @@ -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("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync("base"); + var ex = await Assert.ThrowsAsync(() => context.AuthenticateAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task ChallengeThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ChallengeAsync("base"); + var ex = await Assert.ThrowsAsync(() => context.ChallengeAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task ForbidThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ForbidAsync("base"); + var ex = await Assert.ThrowsAsync(() => context.ForbidAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task CanOnlySignInIfSupported() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("uber", "whatever"); + o.AddScheme("base", "whatever"); + o.AddScheme("signin", "whatever"); + o.AddScheme("signout", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.SignInAsync("uber", new ClaimsPrincipal(), null); + var ex = await Assert.ThrowsAsync(() => 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(() => 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("uber", "whatever"); + o.AddScheme("base", "whatever"); + o.AddScheme("signin", "whatever"); + o.AddScheme("signout", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.SignOutAsync("uber"); + var ex = await Assert.ThrowsAsync(() => 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("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(() => context.SignOutAsync()); + Assert.Contains("cannot be used for SignOutAsync", ex.Message); + ex = await Assert.ThrowsAsync(() => 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("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("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("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(() => 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("forbid", "whatever"); + o.DefaultForbidScheme = "forbid"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ForbidAsync(); + } + + + private class BaseHandler : IAuthenticationHandler + { + public Task 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 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 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 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 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 AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + return Task.FromResult(0); + } + + public Task 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(); + } + } + + } +} diff --git a/src/Http/Authentication.Core/test/Microsoft.AspNetCore.Authentication.Core.Test.csproj b/src/Http/Authentication.Core/test/Microsoft.AspNetCore.Authentication.Core.Test.csproj new file mode 100644 index 0000000000..4819703197 --- /dev/null +++ b/src/Http/Authentication.Core/test/Microsoft.AspNetCore.Authentication.Core.Test.csproj @@ -0,0 +1,12 @@ + + + + $(StandardTestTfms) + + + + + + + + diff --git a/src/Http/Authentication.Core/test/TokenExtensionTests.cs b/src/Http/Authentication.Core/test/TokenExtensionTests.cs new file mode 100644 index 0000000000..7215d526e9 --- /dev/null +++ b/src/Http/Authentication.Core/test/TokenExtensionTests.cs @@ -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(); + 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(); + 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(); + 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(); + 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(); + 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 AuthenticateAsync() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + 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(); + } + } + + } +} diff --git a/src/Http/Headers/src/BaseHeaderParser.cs b/src/Http/Headers/src/BaseHeaderParser.cs new file mode 100644 index 0000000000..f3caaafb70 --- /dev/null +++ b/src/Http/Headers/src/BaseHeaderParser.cs @@ -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 : HttpHeaderParser + { + 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; + } + } +} diff --git a/src/Http/Headers/src/CacheControlHeaderValue.cs b/src/Http/Headers/src/CacheControlHeaderValue.cs new file mode 100644 index 0000000000..81e18faf47 --- /dev/null +++ b/src/Http/Headers/src/CacheControlHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(true, GetCacheControlLength); + + private static readonly Action CheckIsValidTokenAction = CheckIsValidToken; + + private bool _noCache; + private ICollection _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 _privateHeaders; + private bool _mustRevalidate; + private bool _proxyRevalidate; + private IList _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 NoCacheHeaders + { + get + { + if (_noCacheHeaders == null) + { + _noCacheHeaders = new ObjectCollection(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 PrivateHeaders + { + get + { + if (_privateHeaders == null) + { + _privateHeaders = new ObjectCollection(CheckIsValidTokenAction); + } + return _privateHeaders; + } + } + + public bool MustRevalidate + { + get { return _mustRevalidate; } + set { _mustRevalidate = value; } + } + + public bool ProxyRevalidate + { + get { return _proxyRevalidate; } + set { _proxyRevalidate = value; } + } + + public IList Extensions + { + get + { + if (_extensions == null) + { + _extensions = new ObjectCollection(); + } + 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(); + 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 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 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(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 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)); + } + } +} diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs new file mode 100644 index 0000000000..b9292ac1a8 --- /dev/null +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(false, GetDispositionTypeLength); + + // Use list instead of dictionary since we may have multiple parameters with the same name. + private ObjectCollection _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 Parameters + { + get + { + if (_parameters == null) + { + _parameters = new ObjectCollection(); + } + 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)); + } + } + } + + /// + /// Sets both FileName and FileNameStar using encodings appropriate for HTTP headers. + /// + /// + public void SetHttpFileName(StringSegment fileName) + { + if (!StringSegment.IsNullOrEmpty(fileName)) + { + FileName = Sanatize(fileName); + } + else + { + FileName = fileName; + } + FileNameStar = fileName; + } + + /// + /// Sets the FileName parameter using encodings appropriate for MIME headers. + /// The FileNameStar paraemter is removed. + /// + /// + 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. in content-disposition string + // "; 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.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.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))); + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs b/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs new file mode 100644 index 0000000000..9ef74baa0c --- /dev/null +++ b/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs @@ -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 +{ + /// + /// Various extension methods for for identifying the type of the disposition header + /// + public static class ContentDispositionHeaderValueIdentityExtensions + { + /// + /// Checks if the content disposition header is a file disposition + /// + /// The header to check + /// True if the header is file disposition, false otherwise + 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)); + } + + /// + /// Checks if the content disposition header is a form disposition + /// + /// The header to check + /// True if the header is form disposition, false otherwise + 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); + } + } +} diff --git a/src/Http/Headers/src/ContentRangeHeaderValue.cs b/src/Http/Headers/src/ContentRangeHeaderValue.cs new file mode 100644 index 0000000000..99583cdf47 --- /dev/null +++ b/src/Http/Headers/src/ContentRangeHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(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: in ' -/' + 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 and in ' -/' + 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: in ' -/' + 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: in ' -/' + 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: in ' -/' + 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; + } + } +} diff --git a/src/Http/Headers/src/CookieHeaderParser.cs b/src/Http/Headers/src/CookieHeaderParser.cs new file mode 100644 index 0000000000..a94b61d319 --- /dev/null +++ b/src/Http/Headers/src/CookieHeaderParser.cs @@ -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 + { + 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; + } + } +} diff --git a/src/Http/Headers/src/CookieHeaderValue.cs b/src/Http/Headers/src/CookieHeaderValue.cs new file mode 100644 index 0000000000..3061b7d2fa --- /dev/null +++ b/src/Http/Headers/src/CookieHeaderValue.cs @@ -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 ParseList(IList inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static IList ParseStrictList(IList inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + public static bool TryParseList(IList inputs, out IList parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + public static bool TryParseStrictList(IList inputs, out IList 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() >= 0) && (Contract.Result() <= (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(); + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/DateTimeFormatter.cs b/src/Http/Headers/src/DateTimeFormatter.cs new file mode 100644 index 0000000000..06893155bd --- /dev/null +++ b/src/Http/Headers/src/DateTimeFormatter.cs @@ -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); + } + } +} diff --git a/src/Http/Headers/src/EntityTagHeaderValue.cs b/src/Http/Headers/src/EntityTagHeaderValue.cs new file mode 100644 index 0000000000..e46cee3a34 --- /dev/null +++ b/src/Http/Headers/src/EntityTagHeaderValue.cs @@ -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 SingleValueParser + = new GenericHeaderParser(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 MultipleValueParser + = new GenericHeaderParser(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(); + } + + /// + /// Check against another 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). + /// + /// The other value to check against for equality. + /// + /// true if the strength and tag of the two values match, + /// false if the other value is null, is not an , or if there is a mismatch of strength or tag between the two values. + /// + 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(); + } + + /// + /// Compares against another to see if they match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2). + /// + /// The other to compare against. + /// true to use a strong comparison, false to use a weak comparison + /// + /// true if the match for the given comparison type, + /// false if the other value is null or the comparison failed. + /// + 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 ParseList(IList inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static IList ParseStrictList(IList inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + public static bool TryParseList(IList inputs, out IList parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + public static bool TryParseStrictList(IList inputs, out IList 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; + } + } +} diff --git a/src/Http/Headers/src/GenericHeaderParser.cs b/src/Http/Headers/src/GenericHeaderParser.cs new file mode 100644 index 0000000000..a2fbf720f9 --- /dev/null +++ b/src/Http/Headers/src/GenericHeaderParser.cs @@ -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 : BaseHeaderParser + { + 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); + } + } +} diff --git a/src/Http/Headers/src/HeaderNames.cs b/src/Http/Headers/src/HeaderNames.cs new file mode 100644 index 0000000000..fe79d242e8 --- /dev/null +++ b/src/Http/Headers/src/HeaderNames.cs @@ -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"; + } +} diff --git a/src/Http/Headers/src/HeaderQuality.cs b/src/Http/Headers/src/HeaderQuality.cs new file mode 100644 index 0000000000..da86450726 --- /dev/null +++ b/src/Http/Headers/src/HeaderQuality.cs @@ -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 + { + /// + /// Quality factor to indicate a perfect match. + /// + public const double Match = 1.0; + + /// + /// Quality factor to indicate no match. + /// + public const double NoMatch = 0.0; + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs new file mode 100644 index 0000000000..20b4319252 --- /dev/null +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -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 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 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(ICollection x, ICollection y) + { + return AreEqualCollections(x, y, null); + } + + internal static bool AreEqualCollections(ICollection x, ICollection y, IEqualityComparer 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; + } + + /// + /// Try to find a target header value among the set of given header values and parse it as a + /// . + /// + /// + /// The containing the set of header values to search. + /// + /// + /// The target header value to look for. + /// + /// + /// When this method returns, contains the parsed , if the parsing succeeded, or + /// null if the parsing failed. The conversion fails if the was not + /// found or could not be parsed as a . This parameter is passed uninitialized; + /// any value originally supplied in result will be overwritten. + /// + /// + /// true if is found and successfully parsed; otherwise, + /// false. + /// + // 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; + } + + /// + /// Check if a target directive exists among the set of given cache control directives. + /// + /// + /// The containing the set of cache control directives. + /// + /// + /// The target cache control directives to look for. + /// + /// + /// true if is contained in ; + /// otherwise, false. + /// + 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; + } + + /// + /// 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. + /// + /// + /// A string containing a number to convert. + /// + /// + /// 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. + /// + /// true if parsing succeeded; otherwise, false. + 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; + } + } + + /// + /// Try to convert a representation of a positive number to its 64-bit signed + /// integer equivalent. A return value indicates whether the conversion succeeded or failed. + /// + /// + /// A containing a number to convert. + /// + /// + /// 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 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. + /// + /// true if parsing succeeded; otherwise, false. + 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; + } + + /// + /// Converts the non-negative 64-bit numeric value to its equivalent string representation. + /// + /// + /// The number to convert. + /// + /// + /// The string representation of the value of this instance, consisting of a sequence of digits ranging from 0 to 9 with no leading zeroes. + /// + 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] == '"'; + } + + /// + /// Given a quoted-string as defined by the RFC specification, + /// removes quotes and unescapes backslashes and quotes. This assumes that the input is a valid quoted-string. + /// + /// The quoted-string to be unescaped. + /// An unescaped version of the quoted-string. + 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; + } + + /// + /// Escapes a as a quoted-string, which is defined by + /// the RFC specification. + /// + /// + /// 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. + /// + /// The input to be escaped. + /// An escaped version of the quoted-string. + 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."); + } + } + } +} diff --git a/src/Http/Headers/src/HttpHeaderParser.cs b/src/Http/Headers/src/HttpHeaderParser.cs new file mode 100644 index 0000000000..027a9de438 --- /dev/null +++ b/src/Http/Headers/src/HttpHeaderParser.cs @@ -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 + { + 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 ?? "")); + } + return result; + } + + public virtual bool TryParseValues(IList values, out IList parsedValues) + { + return TryParseValues(values, strict: false, parsedValues: out parsedValues); + } + + public virtual bool TryParseStrictValues(IList values, out IList parsedValues) + { + return TryParseValues(values, strict: true, parsedValues: out parsedValues); + } + + protected virtual bool TryParseValues(IList values, bool strict, out IList 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 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(); // 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 ParseValues(IList values) + { + return ParseValues(values, strict: false); + } + + public virtual IList ParseStrictValues(IList values) + { + return ParseValues(values, strict: true); + } + + protected virtual IList ParseValues(IList 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(); + 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(); + } + } +} diff --git a/src/Http/Headers/src/HttpParseResult.cs b/src/Http/Headers/src/HttpParseResult.cs new file mode 100644 index 0000000000..709ae0ce84 --- /dev/null +++ b/src/Http/Headers/src/HttpParseResult.cs @@ -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, + } +} diff --git a/src/Http/Headers/src/HttpRuleParser.cs b/src/Http/Headers/src/HttpRuleParser.cs new file mode 100644 index 0000000000..3741ffa110 --- /dev/null +++ b/src/Http/Headers/src/HttpRuleParser.cs @@ -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* + // CTL = + + 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() >= 0) && (Contract.Result() <= (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() >= 0) && (Contract.Result() <= (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() >= 0) && (Contract.Result() <= (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 = + 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 = + // LWS = [CRLF] 1*( SP | HT ) + // CTL = + // + // 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.Parsed) || + (Contract.ValueAtReturn(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; + } + } +} diff --git a/src/Http/Headers/src/MediaTypeHeaderValue.cs b/src/Http/Headers/src/MediaTypeHeaderValue.cs new file mode 100644 index 0000000000..32074b44cc --- /dev/null +++ b/src/Http/Headers/src/MediaTypeHeaderValue.cs @@ -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 +{ + /// + /// Representation of the media type header. See . + /// + 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 SingleValueParser + = new GenericHeaderParser(false, GetMediaTypeLength); + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(true, GetMediaTypeLength); + + // Use a collection instead of a dictionary since we may have multiple parameters with the same name. + private ObjectCollection _parameters; + private StringSegment _mediaType; + private bool _isReadOnly; + + private MediaTypeHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + + /// + /// Initializes a instance. + /// + /// A representation of a media type. + /// The text provided must be a single media type without parameters. + public MediaTypeHeaderValue(StringSegment mediaType) + { + CheckMediaTypeFormat(mediaType, nameof(mediaType)); + _mediaType = mediaType; + } + + /// + /// Initializes a instance. + /// + /// A representation of a media type. + /// The text provided must be a single media type without parameters. + /// The with the quality of the media type. + public MediaTypeHeaderValue(StringSegment mediaType, double quality) + : this(mediaType) + { + Quality = quality; + } + + /// + /// Gets or sets the value of the charset parameter. Returns + /// if there is no charset. + /// + 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)); + } + } + } + } + + /// + /// Gets or sets the value of the Encoding parameter. Setting the Encoding will set + /// the to . + /// + 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; + } + } + } + + /// + /// Gets or sets the value of the boundary parameter. Returns + /// if there is no boundary. + /// + 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)); + } + } + } + } + + /// + /// Gets or sets the media type's parameters. Returns an empty + /// if there are no parameters. + /// + public IList Parameters + { + get + { + if (_parameters == null) + { + if (IsReadOnly) + { + _parameters = ObjectCollection.EmptyReadOnlyCollection; + } + else + { + _parameters = new ObjectCollection(); + } + } + return _parameters; + } + } + + /// + /// Gets or sets the value of the quality parameter. Returns null + /// if there is no quality. + /// + public double? Quality + { + get { return HeaderUtilities.GetQuality(_parameters); } + set + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + HeaderUtilities.SetQuality(Parameters, value); + } + } + + /// + /// Gets or sets the value of the media type. Returns + /// if there is no media type. + /// + /// + /// For the media type "application/json", the property gives the value + /// "application/json". + /// + public StringSegment MediaType + { + get { return _mediaType; } + set + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + CheckMediaTypeFormat(value, nameof(value)); + _mediaType = value; + } + } + + /// + /// Gets the type of the . + /// + /// + /// For the media type "application/json", the property gives the value "application". + /// + /// See for more details on the type. + public StringSegment Type + { + get + { + return _mediaType.Subsegment(0, _mediaType.IndexOf(ForwardSlashCharacter)); + } + } + + /// + /// Gets the subtype of the . + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value + /// "vnd.example+json". + /// + /// See for more details on the subtype. + public StringSegment SubType + { + get + { + return _mediaType.Subsegment(_mediaType.IndexOf(ForwardSlashCharacter) + 1); + } + } + + /// + /// Gets subtype of the , excluding any structured syntax suffix. Returns + /// if there is no subtype without suffix. + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value + /// "vnd.example". + /// + public StringSegment SubTypeWithoutSuffix + { + get + { + var subType = SubType; + var startOfSuffix = subType.LastIndexOf(PlusCharacter); + if (startOfSuffix == -1) + { + return subType; + } + else + { + return subType.Subsegment(0, startOfSuffix); + } + } + } + + /// + /// Gets the structured syntax suffix of the if it has one. + /// See The RFC documentation on structured syntaxes. + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value + /// "json". + /// + public StringSegment Suffix + { + get + { + var subType = SubType; + var startOfSuffix = subType.LastIndexOf(PlusCharacter); + if (startOfSuffix == -1) + { + return default(StringSegment); + } + else + { + return subType.Subsegment(startOfSuffix + 1); + } + } + } + + + /// + /// Get a of facets of the . Facets are a + /// period separated list of StringSegments in the . + /// See The RFC documentation on facets. + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value: + /// {"vnd", "example"} + /// + public IEnumerable Facets + { + get + { + return SubTypeWithoutSuffix.Split(PeriodCharacterArray); + } + } + + /// + /// Gets whether this matches all types. + /// + public bool MatchesAllTypes => MediaType.Equals(MatchesAllString, StringComparison.Ordinal); + + /// + /// Gets whether this matches all subtypes. + /// + /// + /// For the media type "application/*", this property is true. + /// + /// + /// For the media type "application/json", this property is false. + /// + public bool MatchesAllSubTypes => SubType.Equals(WildcardString, StringComparison.Ordinal); + + /// + /// Gets whether this matches all subtypes, ignoring any structured syntax suffix. + /// + /// + /// For the media type "application/*+json", this property is true. + /// + /// + /// For the media type "application/vnd.example+json", this property is false. + /// + public bool MatchesAllSubTypesWithoutSuffix => + SubTypeWithoutSuffix.Equals(WildcardString, StringComparison.OrdinalIgnoreCase); + + /// + /// Gets whether the is readonly. + /// + public bool IsReadOnly + { + get { return _isReadOnly; } + } + + /// + /// Gets a value indicating whether this is a subset of + /// . 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. + /// + /// The to compare. + /// + /// A value indicating whether this is a subset of + /// . + /// + /// + /// 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". + /// + 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); + } + + /// + /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, + /// while avoiding the cost of re-validating the components. + /// + /// A deep copy. + public MediaTypeHeaderValue Copy() + { + var other = new MediaTypeHeaderValue(); + other._mediaType = _mediaType; + + if (_parameters != null) + { + other._parameters = new ObjectCollection( + _parameters.Select(item => item.Copy())); + } + return other; + } + + /// + /// 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. + /// + /// A deep, read-only, copy. + public MediaTypeHeaderValue CopyAsReadOnly() + { + if (IsReadOnly) + { + return this; + } + + var other = new MediaTypeHeaderValue(); + other._mediaType = _mediaType; + if (_parameters != null) + { + other._parameters = new ObjectCollection( + _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); + } + + /// + /// Takes a media type and parses it into the and its associated parameters. + /// + /// The with the media type. + /// The parsed . + public static MediaTypeHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index); + } + + /// + /// Takes a media type, which can include parameters, and parses it into the and its associated parameters. + /// + /// The with the media type. The media type constructed here must not have an y + /// The parsed + /// True if the value was successfully parsed. + public static bool TryParse(StringSegment input, out MediaTypeHeaderValue parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue); + } + + /// + /// Takes an of and parses it into the and its associated parameters. + /// + /// A list of media types + /// The parsed . + public static IList ParseList(IList inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + /// + /// Takes an of and parses it into the and its associated parameters. + /// Throws if there is invalid data in a string. + /// + /// A list of media types + /// The parsed . + public static IList ParseStrictList(IList inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + /// + /// Takes an of and parses it into the and its associated parameters. + /// + /// A list of media types + /// The parsed . + /// True if the value was successfully parsed. + public static bool TryParseList(IList inputs, out IList parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + /// + /// Takes an of and parses it into the and its associated parameters. + /// + /// A list of media types + /// The parsed . + /// True if the value was successfully parsed. + public static bool TryParseStrictList(IList inputs, out IList 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. in media type string "/; 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. in media type string "/; param1=value1; param2=value2" + var subtypeLength = HttpRuleParser.GetTokenLength(input, current); + + if (subtypeLength == 0) + { + return 0; + } + + // If there is no whitespace between and in / get the media type using + // one Substring call. Otherwise get substrings for and 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); + } + } +} diff --git a/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs b/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs new file mode 100644 index 0000000000..cc34640988 --- /dev/null +++ b/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs @@ -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 +{ + /// + /// Implementation of that can compare accept media type header fields + /// based on their quality values (a.k.a q-values). + /// + public class MediaTypeHeaderValueComparer : IComparer + { + private static readonly MediaTypeHeaderValueComparer _mediaTypeComparer = + new MediaTypeHeaderValueComparer(); + + private MediaTypeHeaderValueComparer() + { + } + + public static MediaTypeHeaderValueComparer QualityComparer + { + get { return _mediaTypeComparer; } + } + + /// + /// + /// 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 following + /// their q-values in the order of specific media types, subtype wildcards, and last any full wildcards. + /// + /// + /// 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 } + /// + 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; + } + } +} diff --git a/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj new file mode 100644 index 0000000000..80b0f49989 --- /dev/null +++ b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj @@ -0,0 +1,17 @@ + + + + HTTP header parser implementations. + netstandard2.0 + $(NoWarn);CS1591 + true + true + http + + + + + + + + diff --git a/src/Http/Headers/src/NameValueHeaderValue.cs b/src/Http/Headers/src/NameValueHeaderValue.cs new file mode 100644 index 0000000000..ba197e986c --- /dev/null +++ b/src/Http/Headers/src/NameValueHeaderValue.cs @@ -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 SingleValueParser + = new GenericHeaderParser(false, GetNameValueLength); + internal static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(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; } } + + /// + /// Provides a copy of this object without the cost of re-validating the values. + /// + /// A copy. + 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 ParseList(IList input) + { + return MultipleValueParser.ParseValues(input); + } + + public static IList ParseStrictList(IList input) + { + return MultipleValueParser.ParseStrictValues(input); + } + + public static bool TryParseList(IList input, out IList parsedValues) + { + return MultipleValueParser.TryParseValues(input, out parsedValues); + } + + public static bool TryParseStrictList(IList input, out IList 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 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 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 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. in name/value string "=". 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. in name/value string "=" + 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 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 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(); + } + } +} diff --git a/src/Http/Headers/src/ObjectCollection.cs b/src/Http/Headers/src/ObjectCollection.cs new file mode 100644 index 0000000000..db5f876b53 --- /dev/null +++ b/src/Http/Headers/src/ObjectCollection.cs @@ -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 allows 'null' values to be added. This is not what we want so we use a custom Collection derived + // type to throw if 'null' gets added. Collection internally uses List which comes at some cost. In addition + // Collection.Add() calls List.InsertItem() which is an O(n) operation (compared to O(1) for List.Add()). + // This type is only used for very small collections (1-2 items) to keep the impact of using Collection small. + internal class ObjectCollection : Collection + { + internal static readonly Action DefaultValidator = CheckNotNull; + internal static readonly ObjectCollection EmptyReadOnlyCollection + = new ObjectCollection(DefaultValidator, isReadOnly: true); + + private readonly Action _validator; + + // We need to create a 'read-only' inner list for Collection to do the right + // thing. + private static IList CreateInnerList(bool isReadOnly, IEnumerable other = null) + { + var list = other == null ? new List() : new List(other); + if (isReadOnly) + { + return new ReadOnlyCollection(list); + } + else + { + return list; + } + } + + public ObjectCollection() + : this(DefaultValidator) + { + } + + public ObjectCollection(Action validator, bool isReadOnly = false) + : base(CreateInnerList(isReadOnly)) + { + _validator = validator; + } + + public ObjectCollection(IEnumerable other, bool isReadOnly = false) + : base(CreateInnerList(isReadOnly, other)) + { + _validator = DefaultValidator; + foreach (T item in Items) + { + _validator(item); + } + } + + public bool IsReadOnly => ((ICollection)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)); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/Properties/AssemblyInfo.cs b/src/Http/Headers/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..c876def487 --- /dev/null +++ b/src/Http/Headers/src/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Http/Headers/src/RangeConditionHeaderValue.cs b/src/Http/Headers/src/RangeConditionHeaderValue.cs new file mode 100644 index 0000000000..f1ebee276c --- /dev/null +++ b/src/Http/Headers/src/RangeConditionHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(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; + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/RangeHeaderValue.cs b/src/Http/Headers/src/RangeHeaderValue.cs new file mode 100644 index 0000000000..934b6b6cc1 --- /dev/null +++ b/src/Http/Headers/src/RangeHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(false, GetRangeLength); + + private StringSegment _unit; + private ICollection _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 Ranges + { + get + { + if (_ranges == null) + { + _ranges = new ObjectCollection(); + } + 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: in '=-, -' + 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; + } + } +} diff --git a/src/Http/Headers/src/RangeItemHeaderValue.cs b/src/Http/Headers/src/RangeItemHeaderValue.cs new file mode 100644 index 0000000000..99fdbfef5c --- /dev/null +++ b/src/Http/Headers/src/RangeItemHeaderValue.cs @@ -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 rangeCollection) + { + Contract.Requires(rangeCollection != null); + Contract.Requires(startIndex >= 0); + Contract.Ensures((Contract.Result() == 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; + } + } +} diff --git a/src/Http/Headers/src/SameSiteMode.cs b/src/Http/Headers/src/SameSiteMode.cs new file mode 100644 index 0000000000..1976386c85 --- /dev/null +++ b/src/Http/Headers/src/SameSiteMode.cs @@ -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 + } +} diff --git a/src/Http/Headers/src/SetCookieHeaderValue.cs b/src/Http/Headers/src/SetCookieHeaderValue.cs new file mode 100644 index 0000000000..f3477648de --- /dev/null +++ b/src/Http/Headers/src/SetCookieHeaderValue.cs @@ -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 SingleValueParser + = new GenericHeaderParser(false, GetSetCookieLength); + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(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); + } + } + + /// + /// Append string representation of this to given + /// . + /// + /// + /// The to receive the string representation of this + /// . + /// + 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 ParseList(IList inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static IList ParseStrictList(IList inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + public static bool TryParseList(IList inputs, out IList parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + public static bool TryParseStrictList(IList inputs, out IList 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 = ; 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 = + 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 = + 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(); + } + } +} \ No newline at end of file diff --git a/src/Http/Headers/src/StringWithQualityHeaderValue.cs b/src/Http/Headers/src/StringWithQualityHeaderValue.cs new file mode 100644 index 0000000000..deba2d2697 --- /dev/null +++ b/src/Http/Headers/src/StringWithQualityHeaderValue.cs @@ -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 SingleValueParser + = new GenericHeaderParser(false, GetStringWithQualityLength); + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(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 ParseList(IList input) + { + return MultipleValueParser.ParseValues(input); + } + + public static IList ParseStrictList(IList input) + { + return MultipleValueParser.ParseStrictValues(input); + } + + public static bool TryParseList(IList input, out IList parsedValues) + { + return MultipleValueParser.TryParseValues(input, out parsedValues); + } + + public static bool TryParseStrictList(IList input, out IList 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: in '; q=' + 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; + } + } +} diff --git a/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs b/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs new file mode 100644 index 0000000000..961cc07841 --- /dev/null +++ b/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs @@ -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 +{ + /// + /// Implementation of 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 + /// for a comparer for media type + /// q-values. + /// + public class StringWithQualityHeaderValueComparer : IComparer + { + private static readonly StringWithQualityHeaderValueComparer _qualityComparer = + new StringWithQualityHeaderValueComparer(); + + private StringWithQualityHeaderValueComparer() + { + } + + public static StringWithQualityHeaderValueComparer QualityComparer + { + get { return _qualityComparer; } + } + + /// + /// Compares two 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 following their q-values ending up with any + /// wild-cards at the end. + /// + /// The first value to compare. + /// The second value to compare + /// The result of the comparison. + 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; + } + } +} diff --git a/src/Http/Headers/src/baseline.netcore.json b/src/Http/Headers/src/baseline.netcore.json new file mode 100644 index 0000000000..476f8150a7 --- /dev/null +++ b/src/Http/Headers/src/baseline.netcore.json @@ -0,0 +1,4110 @@ +{ + "AssemblyIdentity": "Microsoft.Net.Http.Headers, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_NoCache", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_NoCache", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_NoCacheHeaders", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_NoStore", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_NoStore", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SharedMaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SharedMaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxStale", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxStale", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxStaleLimit", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxStaleLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MinFresh", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MinFresh", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_NoTransform", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_NoTransform", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnlyIfCached", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnlyIfCached", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Public", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Public", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Private", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Private", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PrivateHeaders", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MustRevalidate", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MustRevalidate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ProxyRevalidate", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProxyRevalidate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Extensions", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PublicString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PrivateString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MaxAgeString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "SharedMaxAgeString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "NoCacheString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "NoStoreString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MaxStaleString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MinFreshString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "NoTransformString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "OnlyIfCachedString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MustRevalidateString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ProxyRevalidateString", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DispositionType", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DispositionType", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Parameters", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileName", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_FileName", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileNameStar", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_FileNameStar", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CreationDate", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CreationDate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ModificationDate", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ModificationDate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReadDate", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReadDate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Size", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Size", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetHttpFileName", + "Parameters": [ + { + "Name": "fileName", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetMimeFileName", + "Parameters": [ + { + "Name": "fileName", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dispositionType", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValueIdentityExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "IsFileDisposition", + "Parameters": [ + { + "Name": "header", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsFormDisposition", + "Parameters": [ + { + "Name": "header", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Unit", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Unit", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_From", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_To", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Length", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasLength", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasRange", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "from", + "Type": "System.Int64" + }, + { + "Name": "to", + "Type": "System.Int64" + }, + { + "Name": "length", + "Type": "System.Int64" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "length", + "Type": "System.Int64" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "from", + "Type": "System.Int64" + }, + { + "Name": "to", + "Type": "System.Int64" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.CookieHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Value", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.CookieHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.CookieHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Any", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Tag", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsWeak", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Compare", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.Net.Http.Headers.EntityTagHeaderValue" + }, + { + "Name": "useStrongComparison", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "tag", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "tag", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "isWeak", + "Type": "System.Boolean" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.HeaderNames", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Accept", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept\"" + }, + { + "Kind": "Field", + "Name": "AcceptCharset", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept-Charset\"" + }, + { + "Kind": "Field", + "Name": "AcceptEncoding", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept-Encoding\"" + }, + { + "Kind": "Field", + "Name": "AcceptLanguage", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept-Language\"" + }, + { + "Kind": "Field", + "Name": "AcceptRanges", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Accept-Ranges\"" + }, + { + "Kind": "Field", + "Name": "AccessControlAllowCredentials", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Allow-Credentials\"" + }, + { + "Kind": "Field", + "Name": "AccessControlAllowHeaders", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Allow-Headers\"" + }, + { + "Kind": "Field", + "Name": "AccessControlAllowMethods", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Allow-Methods\"" + }, + { + "Kind": "Field", + "Name": "AccessControlAllowOrigin", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Allow-Origin\"" + }, + { + "Kind": "Field", + "Name": "AccessControlExposeHeaders", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Expose-Headers\"" + }, + { + "Kind": "Field", + "Name": "AccessControlMaxAge", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Max-Age\"" + }, + { + "Kind": "Field", + "Name": "AccessControlRequestHeaders", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Request-Headers\"" + }, + { + "Kind": "Field", + "Name": "AccessControlRequestMethod", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Access-Control-Request-Method\"" + }, + { + "Kind": "Field", + "Name": "Age", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Age\"" + }, + { + "Kind": "Field", + "Name": "Allow", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Allow\"" + }, + { + "Kind": "Field", + "Name": "Authority", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":authority\"" + }, + { + "Kind": "Field", + "Name": "Authorization", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Authorization\"" + }, + { + "Kind": "Field", + "Name": "CacheControl", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Cache-Control\"" + }, + { + "Kind": "Field", + "Name": "Connection", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Connection\"" + }, + { + "Kind": "Field", + "Name": "ContentDisposition", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Disposition\"" + }, + { + "Kind": "Field", + "Name": "ContentEncoding", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Encoding\"" + }, + { + "Kind": "Field", + "Name": "ContentLanguage", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Language\"" + }, + { + "Kind": "Field", + "Name": "ContentLength", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Length\"" + }, + { + "Kind": "Field", + "Name": "ContentLocation", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Location\"" + }, + { + "Kind": "Field", + "Name": "ContentMD5", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-MD5\"" + }, + { + "Kind": "Field", + "Name": "ContentRange", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Range\"" + }, + { + "Kind": "Field", + "Name": "ContentSecurityPolicy", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Security-Policy\"" + }, + { + "Kind": "Field", + "Name": "ContentSecurityPolicyReportOnly", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Security-Policy-Report-Only\"" + }, + { + "Kind": "Field", + "Name": "ContentType", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Content-Type\"" + }, + { + "Kind": "Field", + "Name": "Cookie", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Cookie\"" + }, + { + "Kind": "Field", + "Name": "Date", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Date\"" + }, + { + "Kind": "Field", + "Name": "ETag", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"ETag\"" + }, + { + "Kind": "Field", + "Name": "Expires", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Expires\"" + }, + { + "Kind": "Field", + "Name": "Expect", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Expect\"" + }, + { + "Kind": "Field", + "Name": "From", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"From\"" + }, + { + "Kind": "Field", + "Name": "Host", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Host\"" + }, + { + "Kind": "Field", + "Name": "IfMatch", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-Match\"" + }, + { + "Kind": "Field", + "Name": "IfModifiedSince", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-Modified-Since\"" + }, + { + "Kind": "Field", + "Name": "IfNoneMatch", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-None-Match\"" + }, + { + "Kind": "Field", + "Name": "IfRange", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-Range\"" + }, + { + "Kind": "Field", + "Name": "IfUnmodifiedSince", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"If-Unmodified-Since\"" + }, + { + "Kind": "Field", + "Name": "LastModified", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Last-Modified\"" + }, + { + "Kind": "Field", + "Name": "Location", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Location\"" + }, + { + "Kind": "Field", + "Name": "MaxForwards", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Max-Forwards\"" + }, + { + "Kind": "Field", + "Name": "Method", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":method\"" + }, + { + "Kind": "Field", + "Name": "Origin", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Origin\"" + }, + { + "Kind": "Field", + "Name": "Path", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":path\"" + }, + { + "Kind": "Field", + "Name": "Pragma", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Pragma\"" + }, + { + "Kind": "Field", + "Name": "ProxyAuthenticate", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Proxy-Authenticate\"" + }, + { + "Kind": "Field", + "Name": "ProxyAuthorization", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Proxy-Authorization\"" + }, + { + "Kind": "Field", + "Name": "Range", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Range\"" + }, + { + "Kind": "Field", + "Name": "Referer", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Referer\"" + }, + { + "Kind": "Field", + "Name": "RetryAfter", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Retry-After\"" + }, + { + "Kind": "Field", + "Name": "Scheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":scheme\"" + }, + { + "Kind": "Field", + "Name": "Server", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Server\"" + }, + { + "Kind": "Field", + "Name": "SetCookie", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Set-Cookie\"" + }, + { + "Kind": "Field", + "Name": "Status", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\":status\"" + }, + { + "Kind": "Field", + "Name": "StrictTransportSecurity", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Strict-Transport-Security\"" + }, + { + "Kind": "Field", + "Name": "TE", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"TE\"" + }, + { + "Kind": "Field", + "Name": "Trailer", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Trailer\"" + }, + { + "Kind": "Field", + "Name": "TransferEncoding", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Transfer-Encoding\"" + }, + { + "Kind": "Field", + "Name": "Upgrade", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Upgrade\"" + }, + { + "Kind": "Field", + "Name": "UserAgent", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"User-Agent\"" + }, + { + "Kind": "Field", + "Name": "Vary", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Vary\"" + }, + { + "Kind": "Field", + "Name": "Via", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Via\"" + }, + { + "Kind": "Field", + "Name": "Warning", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Warning\"" + }, + { + "Kind": "Field", + "Name": "WebSocketSubProtocols", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Sec-WebSocket-Protocol\"" + }, + { + "Kind": "Field", + "Name": "WWWAuthenticate", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"WWW-Authenticate\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.HeaderQuality", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Match", + "Parameters": [], + "ReturnType": "System.Double", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "NoMatch", + "Parameters": [], + "ReturnType": "System.Double", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "0" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.HeaderUtilities", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "TryParseSeconds", + "Parameters": [ + { + "Name": "headerValues", + "Type": "Microsoft.Extensions.Primitives.StringValues" + }, + { + "Name": "targetValue", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Nullable", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsCacheDirective", + "Parameters": [ + { + "Name": "cacheControlDirectives", + "Type": "Microsoft.Extensions.Primitives.StringValues" + }, + { + "Name": "targetDirectives", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseNonNegativeInt32", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "result", + "Type": "System.Int32", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseNonNegativeInt64", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "result", + "Type": "System.Int64", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatNonNegativeInt64", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseDate", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "result", + "Type": "System.DateTimeOffset", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatDate", + "Parameters": [ + { + "Name": "dateTime", + "Type": "System.DateTimeOffset" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatDate", + "Parameters": [ + { + "Name": "dateTime", + "Type": "System.DateTimeOffset" + }, + { + "Name": "quoted", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoveQuotes", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsQuoted", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UnescapeAsQuotedString", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EscapeAsQuotedString", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Charset", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Charset", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Encoding", + "Parameters": [], + "ReturnType": "System.Text.Encoding", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Encoding", + "Parameters": [ + { + "Name": "value", + "Type": "System.Text.Encoding" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Boundary", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Boundary", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Parameters", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Quality", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Quality", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MediaType", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MediaType", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Type", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SubType", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SubTypeWithoutSuffix", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Suffix", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Facets", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MatchesAllTypes", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MatchesAllSubTypes", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MatchesAllSubTypesWithoutSuffix", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsSubsetOf", + "Parameters": [ + { + "Name": "otherMediaType", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyAsReadOnly", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "mediaType", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "mediaType", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "quality", + "Type": "System.Double" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.MediaTypeHeaderValueComparer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IComparer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_QualityComparer", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValueComparer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Compare", + "Parameters": [ + { + "Name": "mediaType1", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + }, + { + "Name": "mediaType2", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + } + ], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IComparer", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Value", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyAsReadOnly", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetUnescapedValue", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetAndEscapeValue", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Find", + "Parameters": [ + { + "Name": "values", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.NameValueHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_LastModified", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_EntityTag", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "lastModified", + "Type": "System.DateTimeOffset" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "entityTag", + "Type": "Microsoft.Net.Http.Headers.EntityTagHeaderValue" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "entityTag", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.RangeHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Unit", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Unit", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Ranges", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.RangeHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.RangeHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "from", + "Type": "System.Nullable" + }, + { + "Name": "to", + "Type": "System.Nullable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.RangeItemHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_From", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_To", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "from", + "Type": "System.Nullable" + }, + { + "Name": "to", + "Type": "System.Nullable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.SameSiteMode", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "None", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Lax", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "Strict", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.SetCookieHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Value", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expires", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expires", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Domain", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Domain", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Secure", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Secure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SameSite", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.SameSiteMode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SameSite", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.SameSiteMode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendToStringBuilder", + "Parameters": [ + { + "Name": "builder", + "Type": "System.Text.StringBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.SetCookieHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.SetCookieHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "inputs", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Primitives.StringSegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Quality", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Parse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParse", + "Parameters": [ + { + "Name": "input", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "parsedValue", + "Type": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseStrictList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryParseStrictList", + "Parameters": [ + { + "Name": "input", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "parsedValues", + "Type": "System.Collections.Generic.IList", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "quality", + "Type": "System.Double" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValueComparer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IComparer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_QualityComparer", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValueComparer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Compare", + "Parameters": [ + { + "Name": "stringWithQuality1", + "Type": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue" + }, + { + "Name": "stringWithQuality2", + "Type": "Microsoft.Net.Http.Headers.StringWithQualityHeaderValue" + } + ], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IComparer", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Headers/test/CacheControlHeaderValueTest.cs b/src/Http/Headers/test/CacheControlHeaderValueTest.cs new file mode 100644 index 0000000000..51e8ce5f58 --- /dev/null +++ b/src/Http/Headers/test/CacheControlHeaderValueTest.cs @@ -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(() => cacheControl.NoCacheHeaders.Add(null)); + Assert.Throws(() => 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(() => cacheControl.PrivateHeaders.Add(null)); + Assert.Throws(() => 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(() => 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(() => 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 + } +} diff --git a/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs b/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs new file mode 100644 index 0000000000..ad1f7fce1f --- /dev/null +++ b/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs @@ -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(() => 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(() => 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(() => 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(() => 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. ."); + 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 ValidContentDispositionTestCases = new TheoryData() + { + { "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 Section 2.8 of RFC 2183.). + { @"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 Section 2.8 of RFC 2183.). 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 name parameter of foo-%41.html. (this test was added to observe the behavior of the (unspecified) treatment of ""name"" as synonym for ""filename""; see Ned Freed's summary 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 token 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 RFC 2616, Section 4.2, 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(() => 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(() => 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(() => new ContentDispositionHeaderValue(contentDisposition)); + } + } +} diff --git a/src/Http/Headers/test/ContentRangeHeaderValueTest.cs b/src/Http/Headers/test/ContentRangeHeaderValueTest.cs new file mode 100644 index 0000000000..d8abdbdbf6 --- /dev/null +++ b/src/Http/Headers/test/ContentRangeHeaderValueTest.cs @@ -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(() => 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(() => new ContentRangeHeaderValue(-1, 1)); + Assert.Throws(() => new ContentRangeHeaderValue(0, -1)); + Assert.Throws(() => 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(() => new ContentRangeHeaderValue(-1, 1, 2)); + Assert.Throws(() => new ContentRangeHeaderValue(0, -1, 2)); + Assert.Throws(() => new ContentRangeHeaderValue(0, 1, -1)); + Assert.Throws(() => new ContentRangeHeaderValue(2, 1, 3)); + Assert.Throws(() => 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(() => range.Unit = null); + Assert.Throws(() => range.Unit = ""); + Assert.Throws(() => range.Unit = " x"); + Assert.Throws(() => range.Unit = "x "); + Assert.Throws(() => 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. "); + 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(() => 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); + } + } +} diff --git a/src/Http/Headers/test/CookieHeaderValueTest.cs b/src/Http/Headers/test/CookieHeaderValueTest.cs new file mode 100644 index 0000000000..416441991d --- /dev/null +++ b/src/Http/Headers/test/CookieHeaderValueTest.cs @@ -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 CookieHeaderDataSet + { + get + { + var dataset = new TheoryData(); + 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 InvalidCookieHeaderDataSet + { + get + { + return new TheoryData + { + "=value", + "name=value;", + "name=value,", + }; + } + } + + public static TheoryData InvalidCookieNames + { + get + { + return new TheoryData + { + "", + "{acb}", + "[acb]", + "\"acb\"", + "a,b", + "a;b", + "a\\b", + "a b", + }; + } + } + + public static TheoryData InvalidCookieValues + { + get + { + return new TheoryData + { + { "\"" }, + { "a,b" }, + { "a;b" }, + { "a\\b" }, + { "\"abc" }, + { "a\"bc" }, + { "abc\"" }, + { "a b" }, + }; + } + } + + public static TheoryData, string[]> ListOfCookieHeaderDataSet + { + get + { + var dataset = new TheoryData, 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, string[]> ListWithInvalidCookieHeaderDataSet + { + get + { + var dataset = new TheoryData, 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(() => new CookieHeaderValue(null, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieNames))] + public void CookieHeaderValue_CtorThrowsOnInvalidName(string name) + { + Assert.Throws(() => new CookieHeaderValue(name, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieValues))] + public void CookieHeaderValue_CtorThrowsOnInvalidValue(string value) + { + Assert.Throws(() => 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(() => 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 cookies, string[] input) + { + var results = CookieHeaderValue.ParseList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_ParseStrictList_AcceptsValidValues(IList cookies, string[] input) + { + var results = CookieHeaderValue.ParseStrictList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseList_AcceptsValidValues(IList 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 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 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(), results); + } + + [Theory] + [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseList_ExcludesInvalidValues(IList 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 cookies, +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + string[] input) + { + Assert.Throws(() => 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 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); + } + } +} diff --git a/src/Http/Headers/test/DateParserTest.cs b/src/Http/Headers/test/DateParserTest.cs new file mode 100644 index 0000000000..5c211c4368 --- /dev/null +++ b/src/Http/Headers/test/DateParserTest.cs @@ -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 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 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))); + } + } +} diff --git a/src/Http/Headers/test/EntityTagHeaderValueTest.cs b/src/Http/Headers/test/EntityTagHeaderValueTest.cs new file mode 100644 index 0000000000..f633fec226 --- /dev/null +++ b/src/Http/Headers/test/EntityTagHeaderValueTest.cs @@ -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(() => new EntityTagHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => 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. ."); + 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 NotEquivalentUnderStrongComparison + { + get + { + return new TheoryData + { + { 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 EquivalentUnderStrongComparison + { + get + { + return new TheoryData + { + { 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 NotEquivalentUnderWeakComparison + { + get + { + return new TheoryData + { + { 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 EquivalentUnderWeakComparison + { + get + { + return new TheoryData + { + { 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 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 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 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 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 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(() => 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 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 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(() => 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(() => new EntityTagHeaderValue(tag)); + } + } +} diff --git a/src/Http/Headers/test/HeaderUtilitiesTest.cs b/src/Http/Headers/test/HeaderUtilitiesTest.cs new file mode 100644 index 0000000000..848190b02e --- /dev/null +++ b/src/Http/Headers/test/HeaderUtilitiesTest.cs @@ -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 TestValues + { + get + { + var data = new TheoryData(); + + 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(() => 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(() => { var actual = HeaderUtilities.EscapeAsQuotedString(input); }); + } + + [Fact] + public void SetAndEscapeValue_ThrowsFormatExceptionOnDelCharacter() + { + Assert.Throws(() => { var actual = HeaderUtilities.EscapeAsQuotedString($"{(char)0x7F}"); }); + } + } +} diff --git a/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs b/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs new file mode 100644 index 0000000000..3ce2702ec6 --- /dev/null +++ b/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs @@ -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 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 unsorted, IEnumerable 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); + } + } +} diff --git a/src/Http/Headers/test/MediaTypeHeaderValueTest.cs b/src/Http/Headers/test/MediaTypeHeaderValueTest.cs new file mode 100644 index 0000000000..75cccabc9c --- /dev/null +++ b/src/Http/Headers/test/MediaTypeHeaderValueTest.cs @@ -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(() => new MediaTypeHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => 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 MediaTypesWithSuffixes => + new TheoryData + { + // 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 MediaTypesWithSuffixesAndSpaces => + new TheoryData + { + // 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(() => 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(() => { 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(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name"))); + Assert.Throws(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name"))); + Assert.Throws(() => 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(() => new MediaTypeHeaderValue("application/xml", -0.01)); + } + + [Fact] + public void Quality_GreaterThanOne_Throw() + { + var mediaType = new MediaTypeHeaderValue("application/xml"); + Assert.Throws(() => 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. ."); + 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 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 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 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(() => 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 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 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> MediaTypesWithFacets => + new TheoryData> + { + { "application/vdn.github", + new List(){ "vdn", "github" } }, + { "application/vdn.github+json", + new List(){ "vdn", "github" } }, + { "application/vdn.github.v3+json", + new List(){ "vdn", "github", "v3" } }, + { "application/vdn.github.+json", + new List(){ "vdn", "github", "" } }, + }; + + [Theory] + [MemberData(nameof(MediaTypesWithFacets))] + public void Facets_TestPositiveCases(string input, List 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(() => 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(() => new MediaTypeHeaderValue(mediaType)); + } + } +} diff --git a/src/Http/Headers/test/Microsoft.Net.Http.Headers.Tests.csproj b/src/Http/Headers/test/Microsoft.Net.Http.Headers.Tests.csproj new file mode 100644 index 0000000000..eb53233e33 --- /dev/null +++ b/src/Http/Headers/test/Microsoft.Net.Http.Headers.Tests.csproj @@ -0,0 +1,11 @@ + + + + $(StandardTestTfms) + + + + + + + diff --git a/src/Http/Headers/test/NameValueHeaderValueTest.cs b/src/Http/Headers/test/NameValueHeaderValueTest.cs new file mode 100644 index 0000000000..cac18debbb --- /dev/null +++ b/src/Http/Headers/test/NameValueHeaderValueTest.cs @@ -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(() => new NameValueHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => 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(() => { 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(() => { 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(() => { var x = new NameValueHeaderValue("name"); x.Value = " x "; }); + Assert.Throws(() => { 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), " vs. ."); + + nameValue1.Value = "token"; + nameValue2.Value = null; + Assert.False(nameValue1.Equals(nameValue2), "token vs. ."); + + nameValue1.Value = null; + nameValue2.Value = "token"; + Assert.False(nameValue1.Equals(nameValue2), " 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), " 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. ."); + } + + [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 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 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(() => 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 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 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(() => 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(() => 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(() => new NameValueHeaderValue(name, value)); + } + + #endregion + } +} diff --git a/src/Http/Headers/test/RangeConditionHeaderValueTest.cs b/src/Http/Headers/test/RangeConditionHeaderValueTest.cs new file mode 100644 index 0000000000..ce7c73997b --- /dev/null +++ b/src/Http/Headers/test/RangeConditionHeaderValueTest.cs @@ -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(() => 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(() => 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. "); + 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(() => 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 + } +} diff --git a/src/Http/Headers/test/RangeHeaderValueTest.cs b/src/Http/Headers/test/RangeHeaderValueTest.cs new file mode 100644 index 0000000000..92a1d72521 --- /dev/null +++ b/src/Http/Headers/test/RangeHeaderValueTest.cs @@ -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(() => new RangeHeaderValue(5, 2)); + } + + [Fact] + public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation() + { + var range = new RangeHeaderValue(); + range.Unit = "myunit"; + Assert.Equal("myunit", range.Unit); + + Assert.Throws(() => range.Unit = null); + Assert.Throws(() => range.Unit = ""); + Assert.Throws(() => range.Unit = " x"); + Assert.Throws(() => range.Unit = "x "); + Assert.Throws(() => 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. "); + 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(() => 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 + } +} diff --git a/src/Http/Headers/test/RangeItemHeaderValueTest.cs b/src/Http/Headers/test/RangeItemHeaderValueTest.cs new file mode 100644 index 0000000000..95598f0a46 --- /dev/null +++ b/src/Http/Headers/test/RangeItemHeaderValueTest.cs @@ -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(() => new RangeItemHeaderValue(null, null)); + } + + [Fact] + public void Ctor_FromValueNegative_Throw() + { + Assert.Throws(() => new RangeItemHeaderValue(-1, null)); + } + + [Fact] + public void Ctor_FromGreaterThanToValue_Throw() + { + Assert.Throws(() => new RangeItemHeaderValue(2, 1)); + } + + [Fact] + public void Ctor_ToValueNegative_Throw() + { + Assert.Throws(() => 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(1, 2), new Tuple(3, null), + new Tuple(null, 6)); + CheckValidTryParse("1-2,", new Tuple(1, 2)); + CheckValidTryParse("1-", new Tuple(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[] 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); + } + } + } +} diff --git a/src/Http/Headers/test/SetCookieHeaderValueTest.cs b/src/Http/Headers/test/SetCookieHeaderValueTest.cs new file mode 100644 index 0000000000..e7e8bf045a --- /dev/null +++ b/src/Http/Headers/test/SetCookieHeaderValueTest.cs @@ -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 SetCookieHeaderDataSet + { + get + { + var dataset = new TheoryData(); + 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 InvalidSetCookieHeaderDataSet + { + get + { + return new TheoryData + { + "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 InvalidCookieNames + { + get + { + return new TheoryData + { + "", + "{acb}", + "[acb]", + "\"acb\"", + "a,b", + "a;b", + "a\\b", + }; + } + } + + public static TheoryData InvalidCookieValues + { + get + { + return new TheoryData + { + { "\"" }, + { "a,b" }, + { "a;b" }, + { "a\\b" }, + { "\"abc" }, + { "a\"bc" }, + { "abc\"" }, + }; + } + } + + public static TheoryData, string[]> ListOfSetCookieHeaderDataSet + { + get + { + var dataset = new TheoryData, 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, string[]> ListWithInvalidSetCookieHeaderDataSet + { + get + { + var dataset = new TheoryData, 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(() => new SetCookieHeaderValue(null, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieNames))] + public void SetCookieHeaderValue_CtorThrowsOnInvalidName(string name) + { + Assert.Throws(() => new SetCookieHeaderValue(name, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieValues))] + public void SetCookieHeaderValue_CtorThrowsOnInvalidValue(string value) + { + Assert.Throws(() => 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(() => 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 cookies, string[] input) + { + var results = SetCookieHeaderValue.ParseList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseList_AcceptsValidValues(IList 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 cookies, string[] input) + { + var results = SetCookieHeaderValue.ParseStrictList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList 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 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(), results); + } + + [Theory] + [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseList_ExcludesInvalidValues(IList 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 cookies, +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + string[] input) + { + Assert.Throws(() => 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 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); + } + } +} diff --git a/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs b/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs new file mode 100644 index 0000000000..8cda48eef3 --- /dev/null +++ b/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs @@ -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 StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues + { + get + { + return new TheoryData + { + { + 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 unsorted, IEnumerable 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)); + } + } +} diff --git a/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs b/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs new file mode 100644 index 0000000000..49ee58b93e --- /dev/null +++ b/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs @@ -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(() => new StringWithQualityHeaderValue(null)); + Assert.Throws(() => new StringWithQualityHeaderValue("")); + Assert.Throws(() => 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(() => new StringWithQualityHeaderValue(null, 0.1)); + Assert.Throws(() => new StringWithQualityHeaderValue("", 0.1)); + Assert.Throws(() => new StringWithQualityHeaderValue("in valid", 0.1)); + + Assert.Throws(() => new StringWithQualityHeaderValue("t", 1.1)); + Assert.Throws(() => 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. "); + 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(() => 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 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 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 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 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(() => 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 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 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 + } +} diff --git a/src/Http/Http.Abstractions/src/Authentication/AuthenticateInfo.cs b/src/Http/Http.Abstractions/src/Authentication/AuthenticateInfo.cs new file mode 100644 index 0000000000..9e8e3fd537 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Authentication/AuthenticateInfo.cs @@ -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 +{ + /// + /// Used to store the results of an Authenticate call. + /// + public class AuthenticateInfo + { + /// + /// The . + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// The . + /// + public AuthenticationProperties Properties { get; set; } + + /// + /// The . + /// + public AuthenticationDescription Description { get; set; } + } +} diff --git a/src/Http/Http.Abstractions/src/Authentication/AuthenticationDescription.cs b/src/Http/Http.Abstractions/src/Authentication/AuthenticationDescription.cs new file mode 100644 index 0000000000..fb0a073f0b --- /dev/null +++ b/src/Http/Http.Abstractions/src/Authentication/AuthenticationDescription.cs @@ -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 +{ + /// + /// Contains information describing an authentication provider. + /// + public class AuthenticationDescription + { + private const string DisplayNamePropertyKey = "DisplayName"; + private const string AuthenticationSchemePropertyKey = "AuthenticationScheme"; + + /// + /// Initializes a new instance of the class + /// + public AuthenticationDescription() + : this(items: null) + { + } + + /// + /// Initializes a new instance of the class + /// + /// + public AuthenticationDescription(IDictionary items) + { + Items = items ?? new Dictionary(StringComparer.Ordinal); ; + } + + /// + /// Contains metadata about the authentication provider. + /// + public IDictionary Items { get; } + + /// + /// Gets or sets the name used to reference the authentication middleware instance. + /// + public string AuthenticationScheme + { + get { return GetString(AuthenticationSchemePropertyKey); } + set { Items[AuthenticationSchemePropertyKey] = value; } + } + + /// + /// Gets or sets the display name for the authentication provider. + /// + 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; + } + } +} diff --git a/src/Http/Http.Abstractions/src/Authentication/AuthenticationManager.cs b/src/Http/Http.Abstractions/src/Authentication/AuthenticationManager.cs new file mode 100644 index 0000000000..b2916522a5 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Authentication/AuthenticationManager.cs @@ -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 + { + /// + /// Constant used to represent the automatic scheme + /// + public const string AutomaticScheme = "Automatic"; + + public abstract HttpContext HttpContext { get; } + + public abstract IEnumerable GetAuthenticationSchemes(); + + public abstract Task GetAuthenticateInfoAsync(string authenticationScheme); + + // Will remove once callees have been updated + public abstract Task AuthenticateAsync(AuthenticateContext context); + + public virtual async Task 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); + } + + /// + /// Creates a challenge for the authentication manager with . + /// + /// A that represents the asynchronous challenge operation. + 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); + } + + /// + /// Creates a challenge for the authentication manager with . + /// + /// Additional arbitrary values which may be used by particular authentication types. + /// A that represents the asynchronous challenge operation. + 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); + } +} diff --git a/src/Http/Http.Abstractions/src/Authentication/AuthenticationProperties.cs b/src/Http/Http.Abstractions/src/Authentication/AuthenticationProperties.cs new file mode 100644 index 0000000000..881b24fff5 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Authentication/AuthenticationProperties.cs @@ -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 +{ + /// + /// Dictionary used to store state values about the authentication session. + /// + 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"; + + /// + /// Initializes a new instance of the class + /// + public AuthenticationProperties() + : this(items: null) + { + } + + /// + /// Initializes a new instance of the class + /// + /// + public AuthenticationProperties(IDictionary items) + { + Items = items ?? new Dictionary(StringComparer.Ordinal); + } + + /// + /// State values about the authentication session. + /// + public IDictionary Items { get; } + + /// + /// Gets or sets whether the authentication session is persisted across multiple requests. + /// + 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); + } + } + } + } + + /// + /// Gets or sets the full path or absolute URI to be used as an HTTP redirect response value. + /// + 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); + } + } + } + } + + /// + /// Gets or sets the time at which the authentication ticket was issued. + /// + 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); + } + } + } + } + + /// + /// Gets or sets the time at which the authentication ticket expires. + /// + 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); + } + } + } + } + + /// + /// Gets or sets if refreshing the authentication session should be allowed. + /// + 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); + } + } + } + } + } +} diff --git a/src/Http/Http.Abstractions/src/ConnectionInfo.cs b/src/Http/Http.Abstractions/src/ConnectionInfo.cs new file mode 100644 index 0000000000..d4cab49afe --- /dev/null +++ b/src/Http/Http.Abstractions/src/ConnectionInfo.cs @@ -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 + { + /// + /// Gets or sets a unique identifier to represent this connection. + /// + 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 GetClientCertificateAsync(CancellationToken cancellationToken = new CancellationToken()); + } +} diff --git a/src/Http/Http.Abstractions/src/CookieBuilder.cs b/src/Http/Http.Abstractions/src/CookieBuilder.cs new file mode 100644 index 0000000000..ce89e5b054 --- /dev/null +++ b/src/Http/Http.Abstractions/src/CookieBuilder.cs @@ -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 +{ + /// + /// Defines settings used to create a cookie. + /// + public class CookieBuilder + { + private string _name; + + /// + /// The name of the cookie. + /// + public virtual string Name + { + get => _name; + set => _name = !string.IsNullOrEmpty(value) + ? value + : throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value)); + } + + /// + /// The cookie path. + /// + /// + /// Determines the value that will set on . + /// + public virtual string Path { get; set; } + + /// + /// The domain to associate the cookie with. + /// + /// + /// Determines the value that will set on . + /// + public virtual string Domain { get; set; } + + /// + /// Indicates whether a cookie is accessible by client-side script. + /// + /// + /// Determines the value that will set on . + /// + public virtual bool HttpOnly { get; set; } + + /// + /// The SameSite attribute of the cookie. The default value is + /// + /// + /// Determines the value that will set on . + /// + public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.Lax; + + /// + /// The policy that will be used to determine . + /// This is determined from the passed to . + /// + public virtual CookieSecurePolicy SecurePolicy { get; set; } + + /// + /// Gets or sets the lifespan of a cookie. + /// + public virtual TimeSpan? Expiration { get; set; } + + /// + /// Gets or sets the max-age for the cookie. + /// + public virtual TimeSpan? MaxAge { get; set; } + + /// + /// 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. + /// + public virtual bool IsEssential { get; set; } + + /// + /// Creates the cookie options from the given . + /// + /// The . + /// The cookie options. + public CookieOptions Build(HttpContext context) => Build(context, DateTimeOffset.Now); + + /// + /// Creates the cookie options from the given with an expiration based on and . + /// + /// The . + /// The time to use as the base for computing . + /// The cookie options. + 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?) + }; + } + } +} diff --git a/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs b/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs new file mode 100644 index 0000000000..af32d851b0 --- /dev/null +++ b/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs @@ -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 +{ + /// + /// Determines how cookie security properties are set. + /// + public enum CookieSecurePolicy + { + /// + /// 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. + /// + SameAsRequest, + + /// + /// 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. + /// + Always, + + /// + /// 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. + /// + None, + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs new file mode 100644 index 0000000000..5cc06484a2 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs @@ -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 + { + /// + /// Add new values. Each item remains a separate array entry. + /// + /// The to use. + /// The header name. + /// The header value. + public static void Append(this IHeaderDictionary headers, string key, StringValues value) + { + ParsingHelpers.AppendHeaderUnmodified(headers, key, value); + } + + /// + /// Quotes any values containing commas, and then comma joins all of the values with any existing values. + /// + /// The to use. + /// The header name. + /// The header values. + public static void AppendCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) + { + ParsingHelpers.AppendHeaderJoined(headers, key, values); + } + + /// + /// Get the associated values from the collection separated into individual values. + /// Quoted values will not be split, and the quotes will be removed. + /// + /// The to use. + /// The header name. + /// the associated values from the collection separated into individual values, or StringValues.Empty if the key is not present. + public static string[] GetCommaSeparatedValues(this IHeaderDictionary headers, string key) + { + return ParsingHelpers.GetHeaderSplit(headers, key).ToArray(); + } + + /// + /// Quotes any values containing commas, and then comma joins all of the values. + /// + /// The to use. + /// The header name. + /// The header values. + public static void SetCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) + { + ParsingHelpers.SetHeaderJoined(headers, key, values); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs new file mode 100644 index 0000000000..0b24a7d4f4 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs @@ -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 +{ + /// + /// Convenience methods for writing to the response. + /// + public static class HttpResponseWritingExtensions + { + /// + /// Writes the given text to the response body. UTF-8 encoding will be used. + /// + /// The . + /// The text to write to the response. + /// Notifies when request operations should be cancelled. + /// A task that represents the completion of the write operation. + 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); + } + + /// + /// Writes the given text to the response body using the given encoding. + /// + /// The . + /// The text to write to the response. + /// The encoding to use. + /// Notifies when request operations should be cancelled. + /// A task that represents the completion of the write operation. + 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); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs new file mode 100644 index 0000000000..448e2c6f6d --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs @@ -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 +{ + /// + /// Extension methods for the . + /// + public static class MapExtensions + { + /// + /// 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. + /// + /// The instance. + /// The request path to match. + /// The branch to take for positive path matches. + /// The instance. + public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action 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); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs new file mode 100644 index 0000000000..a4f67ce4a2 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs @@ -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 +{ + /// + /// Respresents a middleware that maps a request path to a sub-request pipeline. + /// + public class MapMiddleware + { + private readonly RequestDelegate _next; + private readonly MapOptions _options; + + /// + /// Creates a new instace of . + /// + /// The delegate representing the next middleware in the request pipeline. + /// The middleware options. + 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; + } + + /// + /// Executes the middleware. + /// + /// The for the current request. + /// A task that represents the execution of this middleware. + 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); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs b/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs new file mode 100644 index 0000000000..60adc74379 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs @@ -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 +{ + /// + /// Options for the . + /// + public class MapOptions + { + /// + /// The path to match. + /// + public PathString PathMatch { get; set; } + + /// + /// The branch taken for a positive match. + /// + public RequestDelegate Branch { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs new file mode 100644 index 0000000000..946379df26 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs @@ -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; + + /// + /// Extension methods for the . + /// + public static class MapWhenExtensions + { + /// + /// Branches the request pipeline based on the result of the given predicate. + /// + /// + /// Invoked with the request environment to determine if the branch should be taken + /// Configures a branch to take + /// + public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Predicate predicate, Action 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); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs new file mode 100644 index 0000000000..b012626ba9 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs @@ -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 +{ + /// + /// Respresents a middleware that runs a sub-request pipeline when a given predicate is matched. + /// + public class MapWhenMiddleware + { + private readonly RequestDelegate _next; + private readonly MapWhenOptions _options; + + /// + /// Creates a new instance of . + /// + /// The delegate representing the next middleware in the request pipeline. + /// The middleware options. + 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; + } + + /// + /// Executes the middleware. + /// + /// The for the current request. + /// A task that represents the execution of this middleware. + 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); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs new file mode 100644 index 0000000000..d18eb7f257 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs @@ -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 +{ + /// + /// Options for the . + /// + public class MapWhenOptions + { + private Func _predicate; + + /// + /// The user callback that determines if the branch should be taken. + /// + public Func Predicate + { + get + { + return _predicate; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _predicate = value; + } + } + + /// + /// The branch taken for a positive match. + /// + public RequestDelegate Branch { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs new file mode 100644 index 0000000000..1124043064 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs @@ -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 +{ + /// + /// Extension methods for adding terminal middleware. + /// + public static class RunExtensions + { + /// + /// Adds a terminal middleware delegate to the application's request pipeline. + /// + /// The instance. + /// A delegate that handles the request. + 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); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs new file mode 100644 index 0000000000..c0c9a0f6e5 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs @@ -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 +{ + /// + /// Extension methods for adding middleware. + /// + public static class UseExtensions + { + /// + /// Adds a middleware delegate defined in-line to the application's request pipeline. + /// + /// The instance. + /// A function that handles the request or calls the given next function. + /// The instance. + public static IApplicationBuilder Use(this IApplicationBuilder app, Func, Task> middleware) + { + return app.Use(next => + { + return context => + { + Func simpleNext = () => next(context); + return middleware(context, simpleNext); + }; + }); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs new file mode 100644 index 0000000000..c07fe1e9f1 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs @@ -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 +{ + /// + /// Extension methods for adding typed middleware. + /// + 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); + + /// + /// Adds a middleware type to the application's request pipeline. + /// + /// The middleware type. + /// The instance. + /// The arguments to pass to the middleware type instance's constructor. + /// The instance. + public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, params object[] args) + { + return app.UseMiddleware(typeof(TMiddleware), args); + } + + /// + /// Adds a middleware type to the application's request pipeline. + /// + /// The instance. + /// The middleware type. + /// The arguments to pass to the middleware type instance's constructor. + /// The instance. + 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(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 Compile(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>(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; + } + } +} diff --git a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs new file mode 100644 index 0000000000..482f2f481f --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs @@ -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 +{ + /// + /// Extension methods for . + /// + public static class UsePathBaseExtensions + { + /// + /// Adds a middleware that extracts the specified path base from request path and postpend it to the request path base. + /// + /// The instance. + /// The path base to extract. + /// The instance. + 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(pathBase); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs new file mode 100644 index 0000000000..6474aeda58 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs @@ -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 +{ + /// + /// Represents a middleware that extracts the specified path base from request path and postpend it to the request path base. + /// + public class UsePathBaseMiddleware + { + private readonly RequestDelegate _next; + private readonly PathString _pathBase; + + /// + /// Creates a new instace of . + /// + /// The delegate representing the next middleware in the request pipeline. + /// The path base to extract. + 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; + } + + /// + /// Executes the middleware. + /// + /// The for the current request. + /// A task that represents the execution of this middleware. + 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); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs new file mode 100644 index 0000000000..f506c41c89 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs @@ -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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder +{ + using Predicate = Func; + + /// + /// Extension methods for . + /// + public static class UseWhenExtensions + { + /// + /// Conditionally creates a branch in the request pipeline that is rejoined to the main pipeline. + /// + /// + /// Invoked with the request environment to determine if the branch should be taken + /// Configures a branch to take + /// + public static IApplicationBuilder UseWhen(this IApplicationBuilder app, Predicate predicate, Action 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 and configure the branch builder right away; otherwise, + // we would end up running our branch after all the components + // that were subsequently added to the main builder. + var branchBuilder = app.New(); + configuration(branchBuilder); + + return app.Use(main => + { + // This is called only when the main application builder + // is built, not per request. + branchBuilder.Run(main); + var branch = branchBuilder.Build(); + + return context => + { + if (predicate(context)) + { + return branch(context); + } + else + { + return main(context); + } + }; + }); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/FragmentString.cs b/src/Http/Http.Abstractions/src/FragmentString.cs new file mode 100644 index 0000000000..c1cb306149 --- /dev/null +++ b/src/Http/Http.Abstractions/src/FragmentString.cs @@ -0,0 +1,141 @@ +// 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.Http +{ + /// + /// Provides correct handling for FragmentString value when needed to generate a URI string + /// + public struct FragmentString : IEquatable + { + /// + /// Represents the empty fragment string. This field is read-only. + /// + public static readonly FragmentString Empty = new FragmentString(string.Empty); + + private readonly string _value; + + /// + /// Initialize the fragment string with a given value. This value must be in escaped and delimited format with + /// a leading '#' character. + /// + /// The fragment string to be assigned to the Value property. + public FragmentString(string value) + { + if (!string.IsNullOrEmpty(value) && value[0] != '#') + { + throw new ArgumentException("The leading '#' must be included for a non-empty fragment.", nameof(value)); + } + _value = value; + } + + /// + /// The escaped fragment string with the leading '#' character + /// + public string Value + { + get { return _value; } + } + + /// + /// True if the fragment string is not empty + /// + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } + + /// + /// Provides the fragment string escaped in a way which is correct for combining into the URI representation. + /// A leading '#' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// + /// The fragment string value + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Provides the fragment string escaped in a way which is correct for combining into the URI representation. + /// A leading '#' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// + /// The fragment string value + public string ToUriComponent() + { + // Escape things properly so System.Uri doesn't mis-interpret the data. + return HasValue ? _value : string.Empty; + } + + /// + /// Returns an FragmentString given the fragment as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a fragment. + /// + /// The escaped fragment as it appears in the URI format. + /// The resulting FragmentString + public static FragmentString FromUriComponent(string uriComponent) + { + if (String.IsNullOrEmpty(uriComponent)) + { + return Empty; + } + return new FragmentString(uriComponent); + } + + /// + /// Returns an FragmentString given the fragment as from a Uri object. Relative Uri objects are not supported. + /// + /// The Uri object + /// The resulting FragmentString + public static FragmentString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + string fragmentValue = uri.GetComponents(UriComponents.Fragment, UriFormat.UriEscaped); + if (!string.IsNullOrEmpty(fragmentValue)) + { + fragmentValue = "#" + fragmentValue; + } + return new FragmentString(fragmentValue); + } + + public bool Equals(FragmentString other) + { + if (!HasValue && !other.HasValue) + { + return true; + } + return string.Equals(_value, other._value, StringComparison.Ordinal); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return !HasValue; + } + return obj is FragmentString && Equals((FragmentString)obj); + } + + public override int GetHashCode() + { + return (HasValue ? _value.GetHashCode() : 0); + } + + public static bool operator ==(FragmentString left, FragmentString right) + { + return left.Equals(right); + } + + public static bool operator !=(FragmentString left, FragmentString right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Http/Http.Abstractions/src/HostString.cs b/src/Http/Http.Abstractions/src/HostString.cs new file mode 100644 index 0000000000..9496b26bac --- /dev/null +++ b/src/Http/Http.Abstractions/src/HostString.cs @@ -0,0 +1,379 @@ +// 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; +using Microsoft.AspNetCore.Http.Abstractions; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents the host portion of a URI can be used to construct URI's properly formatted and encoded for use in + /// HTTP headers. + /// + public struct HostString : IEquatable + { + private readonly string _value; + + /// + /// Creates a new HostString without modification. The value should be Unicode rather than punycode, and may have a port. + /// IPv4 and IPv6 addresses are also allowed, and also may have ports. + /// + /// + public HostString(string value) + { + _value = value; + } + + /// + /// Creates a new HostString from its host and port parts. + /// + /// The value should be Unicode rather than punycode. IPv6 addresses must use square braces. + /// A positive, greater than 0 value representing the port in the host string. + public HostString(string host, int port) + { + if(port <= 0) + { + throw new ArgumentOutOfRangeException(nameof(port), Resources.Exception_PortMustBeGreaterThanZero); + } + + int index; + if (host.IndexOf('[') == -1 + && (index = host.IndexOf(':')) >= 0 + && index < host.Length - 1 + && host.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + host = $"[{host}]"; + } + + _value = host + ":" + port.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Returns the original value from the constructor. + /// + public string Value + { + get { return _value; } + } + + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } + + /// + /// Returns the value of the host part of the value. The port is removed if it was present. + /// IPv6 addresses will have brackets added if they are missing. + /// + /// + public string Host + { + get + { + GetParts(_value, out var host, out var port); + + return host.ToString(); + } + } + + /// + /// Returns the value of the port part of the host, or null if none is found. + /// + /// + public int? Port + { + get + { + GetParts(_value, out var host, out var port); + + if (!StringSegment.IsNullOrEmpty(port) + && int.TryParse(port.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out var p)) + { + return p; + } + + return null; + } + } + + /// + /// Returns the value as normalized by ToUriComponent(). + /// + /// + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Returns the value properly formatted and encoded for use in a URI in a HTTP header. + /// Any Unicode is converted to punycode. IPv6 addresses will have brackets added if they are missing. + /// + /// + public string ToUriComponent() + { + if (string.IsNullOrEmpty(_value)) + { + return string.Empty; + } + + int i; + for (i = 0; i < _value.Length; ++i) + { + if (!HostStringHelper.IsSafeHostStringChar(_value[i])) + { + break; + } + } + + if (i != _value.Length) + { + GetParts(_value, out var host, out var port); + + var mapping = new IdnMapping(); + var encoded = mapping.GetAscii(host.Buffer, host.Offset, host.Length); + + return StringSegment.IsNullOrEmpty(port) + ? encoded + : string.Concat(encoded, ":", port.ToString()); + } + + return _value; + } + + /// + /// Creates a new HostString from the given URI component. + /// Any punycode will be converted to Unicode. + /// + /// + /// + public static HostString FromUriComponent(string uriComponent) + { + if (!string.IsNullOrEmpty(uriComponent)) + { + int index; + if (uriComponent.IndexOf('[') >= 0) + { + // IPv6 in brackets [::1], maybe with port + } + else if ((index = uriComponent.IndexOf(':')) >= 0 + && index < uriComponent.Length - 1 + && uriComponent.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + } + else if (uriComponent.IndexOf("xn--", StringComparison.Ordinal) >= 0) + { + // Contains punycode + if (index >= 0) + { + // Has a port + string port = uriComponent.Substring(index); + var mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent, 0, index) + port; + } + else + { + var mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent); + } + } + } + return new HostString(uriComponent); + } + + /// + /// Creates a new HostString from the host and port of the give Uri instance. + /// Punycode will be converted to Unicode. + /// + /// + /// + public static HostString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new HostString(uri.GetComponents( + UriComponents.NormalizedHost | // Always convert punycode to Unicode. + UriComponents.HostAndPort, UriFormat.Unescaped)); + } + + /// + /// Matches the host portion of a host header value against a list of patterns. + /// The host may be the encoded punycode or decoded unicode form so long as the pattern + /// uses the same format. + /// + /// Host header value with or without a port. + /// A set of pattern to match, without ports. + /// + /// The port on the given value is ignored. The patterns should not have ports. + /// The patterns may be exact matches like "example.com", a top level wildcard "*" + /// that matches all hosts, or a subdomain wildcard like "*.example.com" that matches + /// "abc.example.com:443" but not "example.com:443". + /// Matching is case insensitive. + /// + /// + public static bool MatchesAny(StringSegment value, IList patterns) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + if (patterns == null) + { + throw new ArgumentNullException(nameof(patterns)); + } + + // Drop the port + GetParts(value, out var host, out var port); + + for (int i = 0; i < port.Length; i++) + { + if (port[i] < '0' || '9' < port[i]) + { + throw new FormatException($"The given host value '{value}' has a malformed port."); + } + } + + for (int i = 0; i < patterns.Count; i++) + { + var pattern = patterns[i]; + + if (pattern == "*") + { + return true; + } + + if (StringSegment.Equals(pattern, host, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Sub-domain wildcards: *.example.com + if (pattern.StartsWith("*.", StringComparison.Ordinal) && host.Length >= pattern.Length) + { + // .example.com + var allowedRoot = pattern.Subsegment(1); + + var hostRoot = host.Subsegment(host.Length - allowedRoot.Length); + if (hostRoot.Equals(allowedRoot, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + /// + /// Compares the equality of the Value property, ignoring case. + /// + /// + /// + public bool Equals(HostString other) + { + if (!HasValue && !other.HasValue) + { + return true; + } + return string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Compares against the given object only if it is a HostString. + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return !HasValue; + } + return obj is HostString && Equals((HostString)obj); + } + + /// + /// Gets a hash code for the value. + /// + /// + public override int GetHashCode() + { + return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); + } + + /// + /// Compares the two instances for equality. + /// + /// + /// + /// + public static bool operator ==(HostString left, HostString right) + { + return left.Equals(right); + } + + /// + /// Compares the two instances for inequality. + /// + /// + /// + /// + public static bool operator !=(HostString left, HostString right) + { + return !left.Equals(right); + } + + /// + /// Parses the current value. IPv6 addresses will have brackets added if they are missing. + /// + private static void GetParts(StringSegment value, out StringSegment host, out StringSegment port) + { + int index; + port = null; + host = null; + + if (StringSegment.IsNullOrEmpty(value)) + { + return; + } + else if ((index = value.IndexOf(']')) >= 0) + { + // IPv6 in brackets [::1], maybe with port + host = value.Subsegment(0, index + 1); + // Is there a colon and at least one character? + if (index + 2 < value.Length && value[index + 1] == ':') + { + port = value.Subsegment(index + 2); + } + } + else if ((index = value.IndexOf(':')) >= 0 + && index < value.Length - 1 + && value.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + host = $"[{value}]"; + port = null; + } + else if (index >= 0) + { + // Has a port + host = value.Subsegment(0, index); + port = value.Subsegment(index + 1); + } + else + { + host = value; + port = null; + } + } + } +} diff --git a/src/Http/Http.Abstractions/src/HttpContext.cs b/src/Http/Http.Abstractions/src/HttpContext.cs new file mode 100644 index 0000000000..60c938db0a --- /dev/null +++ b/src/Http/Http.Abstractions/src/HttpContext.cs @@ -0,0 +1,87 @@ +// 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; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Encapsulates all HTTP-specific information about an individual HTTP request. + /// + public abstract class HttpContext + { + /// + /// Gets the collection of HTTP features provided by the server and middleware available on this request. + /// + public abstract IFeatureCollection Features { get; } + + /// + /// Gets the object for this request. + /// + public abstract HttpRequest Request { get; } + + /// + /// Gets the object for this request. + /// + public abstract HttpResponse Response { get; } + + /// + /// Gets information about the underlying connection for this request. + /// + public abstract ConnectionInfo Connection { get; } + + /// + /// Gets an object that manages the establishment of WebSocket connections for this request. + /// + public abstract WebSocketManager WebSockets { get; } + + /// + /// This is obsolete and will be removed in a future version. + /// The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. + /// See https://go.microsoft.com/fwlink/?linkid=845470. + /// + [Obsolete("This is obsolete and will be removed in a future version. The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. See https://go.microsoft.com/fwlink/?linkid=845470.")] + public abstract AuthenticationManager Authentication { get; } + + /// + /// Gets or sets the user for this request. + /// + public abstract ClaimsPrincipal User { get; set; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this request. + /// + public abstract IDictionary Items { get; set; } + + /// + /// Gets or sets the that provides access to the request's service container. + /// + public abstract IServiceProvider RequestServices { get; set; } + + /// + /// Notifies when the connection underlying this request is aborted and thus request operations should be + /// cancelled. + /// + public abstract CancellationToken RequestAborted { get; set; } + + /// + /// Gets or sets a unique identifier to represent this request in trace logs. + /// + public abstract string TraceIdentifier { get; set; } + + /// + /// Gets or sets the object used to manage user session data for this request. + /// + public abstract ISession Session { get; set; } + + /// + /// Aborts the connection underlying this request. + /// + public abstract void Abort(); + } +} diff --git a/src/Http/Http.Abstractions/src/HttpMethods.cs b/src/Http/Http.Abstractions/src/HttpMethods.cs new file mode 100644 index 0000000000..1ccee896e7 --- /dev/null +++ b/src/Http/Http.Abstractions/src/HttpMethods.cs @@ -0,0 +1,65 @@ +// 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.Http +{ + public static class HttpMethods + { + public static readonly string Connect = "CONNECT"; + public static readonly string Delete = "DELETE"; + public static readonly string Get = "GET"; + public static readonly string Head = "HEAD"; + public static readonly string Options = "OPTIONS"; + public static readonly string Patch = "PATCH"; + public static readonly string Post = "POST"; + public static readonly string Put = "PUT"; + public static readonly string Trace = "TRACE"; + + public static bool IsConnect(string method) + { + return object.ReferenceEquals(Connect, method) || StringComparer.OrdinalIgnoreCase.Equals(Connect, method); + } + + public static bool IsDelete(string method) + { + return object.ReferenceEquals(Delete, method) || StringComparer.OrdinalIgnoreCase.Equals(Delete, method); + } + + public static bool IsGet(string method) + { + return object.ReferenceEquals(Get, method) || StringComparer.OrdinalIgnoreCase.Equals(Get, method); + } + + public static bool IsHead(string method) + { + return object.ReferenceEquals(Head, method) || StringComparer.OrdinalIgnoreCase.Equals(Head, method); + } + + public static bool IsOptions(string method) + { + return object.ReferenceEquals(Options, method) || StringComparer.OrdinalIgnoreCase.Equals(Options, method); + } + + public static bool IsPatch(string method) + { + return object.ReferenceEquals(Patch, method) || StringComparer.OrdinalIgnoreCase.Equals(Patch, method); + } + + public static bool IsPost(string method) + { + return object.ReferenceEquals(Post, method) || StringComparer.OrdinalIgnoreCase.Equals(Post, method); + } + + public static bool IsPut(string method) + { + return object.ReferenceEquals(Put, method) || StringComparer.OrdinalIgnoreCase.Equals(Put, method); + } + + public static bool IsTrace(string method) + { + return object.ReferenceEquals(Trace, method) || StringComparer.OrdinalIgnoreCase.Equals(Trace, method); + } + } +} diff --git a/src/Http/Http.Abstractions/src/HttpRequest.cs b/src/Http/Http.Abstractions/src/HttpRequest.cs new file mode 100644 index 0000000000..a4337b7766 --- /dev/null +++ b/src/Http/Http.Abstractions/src/HttpRequest.cs @@ -0,0 +1,121 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents the incoming side of an individual HTTP request. + /// + public abstract class HttpRequest + { + /// + /// Gets the for this request. + /// + public abstract HttpContext HttpContext { get; } + + /// + /// Gets or sets the HTTP method. + /// + /// The HTTP method. + public abstract string Method { get; set; } + + /// + /// Gets or sets the HTTP request scheme. + /// + /// The HTTP request scheme. + public abstract string Scheme { get; set; } + + /// + /// Returns true if the RequestScheme is https. + /// + /// true if this request is using https; otherwise, false. + public abstract bool IsHttps { get; set; } + + /// + /// Gets or sets the Host header. May include the port. + /// + /// The Host header. + public abstract HostString Host { get; set; } + + /// + /// Gets or sets the RequestPathBase. + /// + /// The RequestPathBase. + public abstract PathString PathBase { get; set; } + + /// + /// Gets or sets the request path from RequestPath. + /// + /// The request path from RequestPath. + public abstract PathString Path { get; set; } + + /// + /// Gets or sets the raw query string used to create the query collection in Request.Query. + /// + /// The raw query string. + public abstract QueryString QueryString { get; set; } + + /// + /// Gets the query value collection parsed from Request.QueryString. + /// + /// The query value collection parsed from Request.QueryString. + public abstract IQueryCollection Query { get; set; } + + /// + /// Gets or sets the RequestProtocol. + /// + /// The RequestProtocol. + public abstract string Protocol { get; set; } + + /// + /// Gets the request headers. + /// + /// The request headers. + public abstract IHeaderDictionary Headers { get; } + + /// + /// Gets the collection of Cookies for this request. + /// + /// The collection of Cookies for this request. + public abstract IRequestCookieCollection Cookies { get; set; } + + /// + /// Gets or sets the Content-Length header. + /// + /// The value of the Content-Length header, if any. + public abstract long? ContentLength { get; set; } + + /// + /// Gets or sets the Content-Type header. + /// + /// The Content-Type header. + public abstract string ContentType { get; set; } + + /// + /// Gets or sets the RequestBody Stream. + /// + /// The RequestBody Stream. + public abstract Stream Body { get; set; } + + /// + /// Checks the Content-Type header for form types. + /// + /// true if the Content-Type header represents a form content type; otherwise, false. + public abstract bool HasFormContentType { get; } + + /// + /// Gets or sets the request body as a form. + /// + public abstract IFormCollection Form { get; set; } + + /// + /// Reads the request body if it is a form. + /// + /// + public abstract Task ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()); + } +} diff --git a/src/Http/Http.Abstractions/src/HttpResponse.cs b/src/Http/Http.Abstractions/src/HttpResponse.cs new file mode 100644 index 0000000000..8a1e5d4908 --- /dev/null +++ b/src/Http/Http.Abstractions/src/HttpResponse.cs @@ -0,0 +1,109 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents the outgoing side of an individual HTTP request. + /// + public abstract class HttpResponse + { + private static readonly Func _callbackDelegate = callback => ((Func)callback)(); + private static readonly Func _disposeDelegate = disposable => + { + ((IDisposable)disposable).Dispose(); + return Task.CompletedTask; + }; + + /// + /// Gets the for this response. + /// + public abstract HttpContext HttpContext { get; } + + /// + /// Gets or sets the HTTP response code. + /// + public abstract int StatusCode { get; set; } + + /// + /// Gets the response headers. + /// + public abstract IHeaderDictionary Headers { get; } + + /// + /// Gets or sets the response body . + /// + public abstract Stream Body { get; set; } + + /// + /// Gets or sets the value for the Content-Length response header. + /// + public abstract long? ContentLength { get; set; } + + /// + /// Gets or sets the value for the Content-Type response header. + /// + public abstract string ContentType { get; set; } + + /// + /// Gets an object that can be used to manage cookies for this response. + /// + public abstract IResponseCookies Cookies { get; } + + /// + /// Gets a value indicating whether response headers have been sent to the client. + /// + public abstract bool HasStarted { get; } + + /// + /// Adds a delegate to be invoked just before response headers will be sent to the client. + /// + /// The delegate to execute. + /// A state object to capture and pass back to the delegate. + public abstract void OnStarting(Func callback, object state); + + /// + /// Adds a delegate to be invoked just before response headers will be sent to the client. + /// + /// The delegate to execute. + public virtual void OnStarting(Func callback) => OnStarting(_callbackDelegate, callback); + + /// + /// Adds a delegate to be invoked after the response has finished being sent to the client. + /// + /// The delegate to invoke. + /// A state object to capture and pass back to the delegate. + public abstract void OnCompleted(Func callback, object state); + + /// + /// Registers an object for disposal by the host once the request has finished processing. + /// + /// The object to be disposed. + public virtual void RegisterForDispose(IDisposable disposable) => OnCompleted(_disposeDelegate, disposable); + + /// + /// Adds a delegate to be invoked after the response has finished being sent to the client. + /// + /// The delegate to invoke. + public virtual void OnCompleted(Func callback) => OnCompleted(_callbackDelegate, callback); + + /// + /// Returns a temporary redirect response (HTTP 302) to the client. + /// + /// The URL to redirect the client to. This must be properly encoded for use in http headers + /// where only ASCII characters are allowed. + public virtual void Redirect(string location) => Redirect(location, permanent: false); + + /// + /// Returns a redirect response (HTTP 301 or HTTP 302) to the client. + /// + /// The URL to redirect the client to. This must be properly encoded for use in http headers + /// where only ASCII characters are allowed. + /// True if the redirect is permanent (301), otherwise false (302). + public abstract void Redirect(string location, bool permanent); + } +} diff --git a/src/Http/Http.Abstractions/src/IApplicationBuilder.cs b/src/Http/Http.Abstractions/src/IApplicationBuilder.cs new file mode 100644 index 0000000000..6110d7f3db --- /dev/null +++ b/src/Http/Http.Abstractions/src/IApplicationBuilder.cs @@ -0,0 +1,51 @@ +// 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; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Defines a class that provides the mechanisms to configure an application's request pipeline. + /// + public interface IApplicationBuilder + { + /// + /// Gets or sets the that provides access to the application's service container. + /// + IServiceProvider ApplicationServices { get; set; } + + /// + /// Gets the set of HTTP features the application's server provides. + /// + IFeatureCollection ServerFeatures { get; } + + /// + /// Gets a key/value collection that can be used to share data between middleware. + /// + IDictionary Properties { get; } + + /// + /// Adds a middleware delegate to the application's request pipeline. + /// + /// The middleware delegate. + /// The . + IApplicationBuilder Use(Func middleware); + + /// + /// Creates a new that shares the of this + /// . + /// + /// The new . + IApplicationBuilder New(); + + /// + /// Builds the delegate used by this application to process HTTP requests. + /// + /// The request handling delegate. + RequestDelegate Build(); + } +} diff --git a/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs b/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs new file mode 100644 index 0000000000..dc8ec34a7c --- /dev/null +++ b/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs @@ -0,0 +1,10 @@ +// 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 +{ + public interface IHttpContextAccessor + { + HttpContext HttpContext { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/IHttpContextFactory.cs b/src/Http/Http.Abstractions/src/IHttpContextFactory.cs new file mode 100644 index 0000000000..7d049626c3 --- /dev/null +++ b/src/Http/Http.Abstractions/src/IHttpContextFactory.cs @@ -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. + +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http +{ + public interface IHttpContextFactory + { + HttpContext Create(IFeatureCollection featureCollection); + void Dispose(HttpContext httpContext); + } +} diff --git a/src/Http/Http.Abstractions/src/IMiddleware.cs b/src/Http/Http.Abstractions/src/IMiddleware.cs new file mode 100644 index 0000000000..f92527f3f5 --- /dev/null +++ b/src/Http/Http.Abstractions/src/IMiddleware.cs @@ -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.Http +{ + /// + /// Defines middleware that can be added to the application's request pipeline. + /// + public interface IMiddleware + { + /// + /// Request handling method. + /// + /// The for the current request. + /// The delegate representing the remaining middleware in the request pipeline. + /// A that represents the execution of this middleware. + Task InvokeAsync(HttpContext context, RequestDelegate next); + } +} diff --git a/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs b/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs new file mode 100644 index 0000000000..5d9fda8a75 --- /dev/null +++ b/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs @@ -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; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Provides methods to create middleware. + /// + public interface IMiddlewareFactory + { + /// + /// Creates a middleware instance for each request. + /// + /// The concrete of the . + /// The instance. + IMiddleware Create(Type middlewareType); + + /// + /// Releases a instance at the end of each request. + /// + /// The instance to release. + void Release(IMiddleware middleware); + } +} diff --git a/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs b/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs new file mode 100644 index 0000000000..eed9d80f88 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs @@ -0,0 +1,66 @@ +// 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.AspNetCore.Http.Internal +{ + public struct HeaderSegment : IEquatable + { + private readonly StringSegment _formatting; + private readonly StringSegment _data; + + // + // Initializes a new instance of the structure. + // + public HeaderSegment(StringSegment formatting, StringSegment data) + { + _formatting = formatting; + _data = data; + } + + public StringSegment Formatting + { + get { return _formatting; } + } + + public StringSegment Data + { + get { return _data; } + } + + public bool Equals(HeaderSegment other) + { + return _formatting.Equals(other._formatting) && _data.Equals(other._data); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegment && Equals((HeaderSegment)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (_formatting.GetHashCode() * 397) ^ _data.GetHashCode(); + } + } + + public static bool operator ==(HeaderSegment left, HeaderSegment right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegment left, HeaderSegment right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs b/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs new file mode 100644 index 0000000000..40c40a8eb3 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs @@ -0,0 +1,297 @@ +// 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; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public struct HeaderSegmentCollection : IEnumerable, IEquatable + { + private readonly StringValues _headers; + + public HeaderSegmentCollection(StringValues headers) + { + _headers = headers; + } + + public bool Equals(HeaderSegmentCollection other) + { + return StringValues.Equals(_headers, other._headers); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegmentCollection && Equals((HeaderSegmentCollection)obj); + } + + public override int GetHashCode() + { + return (!StringValues.IsNullOrEmpty(_headers) ? _headers.GetHashCode() : 0); + } + + public static bool operator ==(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return !left.Equals(right); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(_headers); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + private readonly StringValues _headers; + private int _index; + + private string _header; + private int _headerLength; + private int _offset; + + private int _leadingStart; + private int _leadingEnd; + private int _valueStart; + private int _valueEnd; + private int _trailingStart; + + private Mode _mode; + + public Enumerator(StringValues headers) + { + _headers = headers; + _header = string.Empty; + _headerLength = -1; + _index = -1; + _offset = -1; + _leadingStart = -1; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + _mode = Mode.Leading; + } + + private enum Mode + { + Leading, + Value, + ValueQuoted, + Trailing, + Produce, + } + + private enum Attr + { + Value, + Quote, + Delimiter, + Whitespace + } + + public HeaderSegment Current + { + get + { + return new HeaderSegment( + new StringSegment(_header, _leadingStart, _leadingEnd - _leadingStart), + new StringSegment(_header, _valueStart, _valueEnd - _valueStart)); + } + } + + object IEnumerator.Current + { + get { return Current; } + } + + public void Dispose() + { + } + + public bool MoveNext() + { + while (true) + { + if (_mode == Mode.Produce) + { + _leadingStart = _trailingStart; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + if (_offset == _headerLength && + _leadingStart != -1 && + _leadingStart != _offset) + { + // Also produce trailing whitespace + _leadingEnd = _offset; + return true; + } + _mode = Mode.Leading; + } + + // if end of a string + if (_offset == _headerLength) + { + ++_index; + _offset = -1; + _leadingStart = 0; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + // if that was the last string + if (_index == _headers.Count) + { + // no more move nexts + return false; + } + + // grab the next string + _header = _headers[_index] ?? string.Empty; + _headerLength = _header.Length; + } + while (true) + { + ++_offset; + char ch = _offset == _headerLength ? (char)0 : _header[_offset]; + // todo - array of attrs + Attr attr = char.IsWhiteSpace(ch) ? Attr.Whitespace : ch == '\"' ? Attr.Quote : (ch == ',' || ch == (char)0) ? Attr.Delimiter : Attr.Value; + + switch (_mode) + { + case Mode.Leading: + switch (attr) + { + case Attr.Delimiter: + _valueStart = _valueStart == -1 ? _offset : _valueStart; + _valueEnd = _valueEnd == -1 ? _offset : _valueEnd; + _trailingStart = _trailingStart == -1 ? _offset : _trailingStart; + _leadingEnd = _offset; + _mode = Mode.Produce; + break; + case Attr.Quote: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Value: + switch (attr) + { + case Attr.Quote: + _mode = Mode.ValueQuoted; + break; + case Attr.Delimiter: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + break; + case Attr.Value: + // more + break; + case Attr.Whitespace: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Trailing; + break; + } + break; + case Mode.ValueQuoted: + switch (attr) + { + case Attr.Quote: + _mode = Mode.Value; + break; + case Attr.Delimiter: + if (ch == (char)0) + { + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + } + break; + case Attr.Value: + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Trailing: + switch (attr) + { + case Attr.Delimiter: + _mode = Mode.Produce; + break; + case Attr.Quote: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + } + if (_mode == Mode.Produce) + { + return true; + } + } + } + } + + public void Reset() + { + _index = 0; + _offset = 0; + _leadingStart = 0; + _leadingEnd = 0; + _valueStart = 0; + _valueEnd = 0; + } + } + } + +} diff --git a/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs b/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs new file mode 100644 index 0000000000..f4cfac52af --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs @@ -0,0 +1,36 @@ +// 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.Internal +{ + internal class HostStringHelper + { + // Allowed Characters: + // A-Z, a-z, 0-9, ., + // -, %, [, ], : + // Above for IPV6 + private static bool[] SafeHostStringChars = { + false, false, false, false, false, false, false, false, // 0x00 - 0x07 + false, false, false, false, false, false, false, false, // 0x08 - 0x0F + false, false, false, false, false, false, false, false, // 0x10 - 0x17 + false, false, false, false, false, false, false, false, // 0x18 - 0x1F + false, false, false, false, false, true, false, false, // 0x20 - 0x27 + false, false, false, false, false, true, true, false, // 0x28 - 0x2F + true, true, true, true, true, true, true, true, // 0x30 - 0x37 + true, true, true, false, false, false, false, false, // 0x38 - 0x3F + false, true, true, true, true, true, true, true, // 0x40 - 0x47 + true, true, true, true, true, true, true, true, // 0x48 - 0x4F + true, true, true, true, true, true, true, true, // 0x50 - 0x57 + true, true, true, true, false, true, false, false, // 0x58 - 0x5F + false, true, true, true, true, true, true, true, // 0x60 - 0x67 + true, true, true, true, true, true, true, true, // 0x68 - 0x6F + true, true, true, true, true, true, true, true, // 0x70 - 0x77 + true, true, true, false, false, false, false, false, // 0x78 - 0x7F + }; + + public static bool IsSafeHostStringChar(char c) + { + return c < SafeHostStringChars.Length && SafeHostStringChars[c]; + } + } +} diff --git a/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs new file mode 100644 index 0000000000..185fc40ac7 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs @@ -0,0 +1,165 @@ +// 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; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public static class ParsingHelpers + { + public static StringValues GetHeader(IHeaderDictionary headers, string key) + { + StringValues value; + return headers.TryGetValue(key, out value) ? value : StringValues.Empty; + } + + public static StringValues GetHeaderSplit(IHeaderDictionary headers, string key) + { + var values = GetHeaderUnmodified(headers, key); + return new StringValues(GetHeaderSplitImplementation(values).ToArray()); + } + + private static IEnumerable GetHeaderSplitImplementation(StringValues values) + { + foreach (var segment in new HeaderSegmentCollection(values)) + { + if (!StringSegment.IsNullOrEmpty(segment.Data)) + { + var value = DeQuote(segment.Data.Value); + if (!string.IsNullOrEmpty(value)) + { + yield return value; + } + } + } + } + + public static StringValues GetHeaderUnmodified(IHeaderDictionary headers, string key) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + StringValues values; + return headers.TryGetValue(key, out values) ? values : StringValues.Empty; + } + + public static void SetHeaderJoined(IHeaderDictionary headers, string key, StringValues value) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + if (StringValues.IsNullOrEmpty(value)) + { + headers.Remove(key); + } + else + { + headers[key] = string.Join(",", value.Select((s) => QuoteIfNeeded(s))); + } + } + + // Quote items that contain commas and are not already quoted. + private static string QuoteIfNeeded(string value) + { + if (!string.IsNullOrEmpty(value) && + value.Contains(',') && + (value[0] != '"' || value[value.Length - 1] != '"')) + { + return $"\"{value}\""; + } + return value; + } + + private static string DeQuote(string value) + { + if (!string.IsNullOrEmpty(value) && + (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')) + { + value = value.Substring(1, value.Length - 2); + } + + return value; + } + + public static void SetHeaderUnmodified(IHeaderDictionary headers, string key, StringValues? values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + if (!values.HasValue || StringValues.IsNullOrEmpty(values.Value)) + { + headers.Remove(key); + } + else + { + headers[key] = values.Value; + } + } + + public static void AppendHeaderJoined(IHeaderDictionary headers, string key, params string[] values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (values == null || values.Length == 0) + { + return; + } + + string existing = GetHeader(headers, key); + if (existing == null) + { + SetHeaderJoined(headers, key, values); + } + else + { + headers[key] = existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))); + } + } + + public static void AppendHeaderUnmodified(IHeaderDictionary headers, string key, StringValues values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (values.Count == 0) + { + return; + } + + var existing = GetHeaderUnmodified(headers, key); + SetHeaderUnmodified(headers, key, StringValues.Concat(existing, values)); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs b/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs new file mode 100644 index 0000000000..b6cebb2b9c --- /dev/null +++ b/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs @@ -0,0 +1,47 @@ +// 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.Internal +{ + internal class PathStringHelper + { + private static bool[] ValidPathChars = { + false, false, false, false, false, false, false, false, // 0x00 - 0x07 + false, false, false, false, false, false, false, false, // 0x08 - 0x0F + false, false, false, false, false, false, false, false, // 0x10 - 0x17 + false, false, false, false, false, false, false, false, // 0x18 - 0x1F + false, true, false, false, true, false, true, true, // 0x20 - 0x27 + true, true, true, true, true, true, true, true, // 0x28 - 0x2F + true, true, true, true, true, true, true, true, // 0x30 - 0x37 + true, true, true, true, false, true, false, false, // 0x38 - 0x3F + true, true, true, true, true, true, true, true, // 0x40 - 0x47 + true, true, true, true, true, true, true, true, // 0x48 - 0x4F + true, true, true, true, true, true, true, true, // 0x50 - 0x57 + true, true, true, false, false, false, false, true, // 0x58 - 0x5F + false, true, true, true, true, true, true, true, // 0x60 - 0x67 + true, true, true, true, true, true, true, true, // 0x68 - 0x6F + true, true, true, true, true, true, true, true, // 0x70 - 0x77 + true, true, true, false, false, false, true, false, // 0x78 - 0x7F + }; + + public static bool IsValidPathChar(char c) + { + return c < ValidPathChars.Length && ValidPathChars[c]; + } + + public static bool IsPercentEncodedChar(string str, int index) + { + return index < str.Length - 2 + && str[index] == '%' + && IsHexadecimalChar(str[index + 1]) + && IsHexadecimalChar(str[index + 2]); + } + + public static bool IsHexadecimalChar(char c) + { + return ('0' <= c && c <= '9') + || ('A' <= c && c <= 'F') + || ('a' <= c && c <= 'f'); + } + } +} diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj new file mode 100644 index 0000000000..821b40cb19 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -0,0 +1,23 @@ + + + + + ASP.NET Core HTTP object model for HTTP requests and responses and also common extension methods for registering middleware in an IApplicationBuilder. +Commonly used types: +Microsoft.AspNetCore.Builder.IApplicationBuilder +Microsoft.AspNetCore.Http.HttpContext +Microsoft.AspNetCore.Http.HttpRequest +Microsoft.AspNetCore.Http.HttpResponse + netstandard2.0 + true + aspnetcore + $(NoWarn);CS1591 + + + + + + + + + diff --git a/src/Http/Http.Abstractions/src/PathString.cs b/src/Http/Http.Abstractions/src/PathString.cs new file mode 100644 index 0000000000..2a5960b661 --- /dev/null +++ b/src/Http/Http.Abstractions/src/PathString.cs @@ -0,0 +1,477 @@ +// 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.ComponentModel; +using System.Globalization; +using System.Text; +using Microsoft.AspNetCore.Http.Abstractions; +using Microsoft.AspNetCore.Http.Internal; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Provides correct escaping for Path and PathBase values when needed to reconstruct a request or redirect URI string + /// + [TypeConverter(typeof(PathStringConverter))] + public struct PathString : IEquatable + { + private static readonly char[] splitChar = { '/' }; + + /// + /// Represents the empty path. This field is read-only. + /// + public static readonly PathString Empty = new PathString(string.Empty); + + private readonly string _value; + + /// + /// Initalize the path string with a given value. This value must be in unescaped format. Use + /// PathString.FromUriComponent(value) if you have a path value which is in an escaped format. + /// + /// The unescaped path to be assigned to the Value property. + public PathString(string value) + { + if (!string.IsNullOrEmpty(value) && value[0] != '/') + { + throw new ArgumentException(Resources.FormatException_PathMustStartWithSlash(nameof(value)), nameof(value)); + } + _value = value; + } + + /// + /// The unescaped path value + /// + public string Value + { + get { return _value; } + } + + /// + /// True if the path is not empty + /// + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } + + /// + /// Provides the path string escaped in a way which is correct for combining into the URI representation. + /// + /// The escaped path value + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Provides the path string escaped in a way which is correct for combining into the URI representation. + /// + /// The escaped path value + public string ToUriComponent() + { + if (!HasValue) + { + return string.Empty; + } + + StringBuilder buffer = null; + + var start = 0; + var count = 0; + var requiresEscaping = false; + var i = 0; + + while (i < _value.Length) + { + var isPercentEncodedChar = PathStringHelper.IsPercentEncodedChar(_value, i); + if (PathStringHelper.IsValidPathChar(_value[i]) || isPercentEncodedChar) + { + if (requiresEscaping) + { + // the current segment requires escape + if (buffer == null) + { + buffer = new StringBuilder(_value.Length * 3); + } + + buffer.Append(Uri.EscapeDataString(_value.Substring(start, count))); + + requiresEscaping = false; + start = i; + count = 0; + } + + if (isPercentEncodedChar) + { + count += 3; + i += 3; + } + else + { + count++; + i++; + } + } + else + { + if (!requiresEscaping) + { + // the current segument doesn't require escape + if (buffer == null) + { + buffer = new StringBuilder(_value.Length * 3); + } + + buffer.Append(_value, start, count); + + requiresEscaping = true; + start = i; + count = 0; + } + + count++; + i++; + } + } + + if (count == _value.Length && !requiresEscaping) + { + return _value; + } + else + { + if (count > 0) + { + if (buffer == null) + { + buffer = new StringBuilder(_value.Length * 3); + } + + if (requiresEscaping) + { + buffer.Append(Uri.EscapeDataString(_value.Substring(start, count))); + } + else + { + buffer.Append(_value, start, count); + } + } + + return buffer.ToString(); + } + } + + + /// + /// Returns an PathString given the path as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a path. + /// + /// The escaped path as it appears in the URI format. + /// The resulting PathString + public static PathString FromUriComponent(string uriComponent) + { + // REVIEW: what is the exactly correct thing to do? + return new PathString(Uri.UnescapeDataString(uriComponent)); + } + + /// + /// Returns an PathString given the path as from a Uri object. Relative Uri objects are not supported. + /// + /// The Uri object + /// The resulting PathString + public static PathString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + // REVIEW: what is the exactly correct thing to do? + return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)); + } + + /// + /// Determines whether the beginning of this instance matches the specified . + /// + /// The to compare. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines whether the beginning of this instance matches the specified when compared + /// using the specified comparison option. + /// + /// The to compare. + /// One of the enumeration values that determines how this and value are compared. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, StringComparison comparisonType) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) + { + return value1.Length == value2.Length || value1[value2.Length] == '/'; + } + return false; + } + + /// + /// Determines whether the beginning of this instance matches the specified and returns + /// the remaining segments. + /// + /// The to compare. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, out PathString remaining) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out remaining); + } + + /// + /// Determines whether the beginning of this instance matches the specified when compared + /// using the specified comparison option and returns the remaining segments. + /// + /// The to compare. + /// One of the enumeration values that determines how this and value are compared. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString remaining) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) + { + if (value1.Length == value2.Length || value1[value2.Length] == '/') + { + remaining = new PathString(value1.Substring(value2.Length)); + return true; + } + } + remaining = Empty; + return false; + } + + /// + /// Determines whether the beginning of this instance matches the specified and returns + /// the matched and remaining segments. + /// + /// The to compare. + /// The matched segments with the original casing in the source value. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining); + } + + /// + /// Determines whether the beginning of this instance matches the specified when compared + /// using the specified comparison option and returns the matched and remaining segments. + /// + /// The to compare. + /// One of the enumeration values that determines how this and value are compared. + /// The matched segments with the original casing in the source value. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString matched, out PathString remaining) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) + { + if (value1.Length == value2.Length || value1[value2.Length] == '/') + { + matched = new PathString(value1.Substring(0, value2.Length)); + remaining = new PathString(value1.Substring(value2.Length)); + return true; + } + } + remaining = Empty; + matched = Empty; + return false; + } + + /// + /// Adds two PathString instances into a combined PathString value. + /// + /// The combined PathString value + public PathString Add(PathString other) + { + if (HasValue && + other.HasValue && + Value[Value.Length - 1] == '/') + { + // If the path string has a trailing slash and the other string has a leading slash, we need + // to trim one of them. + return new PathString(Value + other.Value.Substring(1)); + } + + return new PathString(Value + other.Value); + } + + /// + /// Combines a PathString and QueryString into the joined URI formatted string value. + /// + /// The joined URI formatted string value + public string Add(QueryString other) + { + return ToUriComponent() + other.ToUriComponent(); + } + + /// + /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. + /// + /// The second PathString for comparison. + /// True if both PathString values are equal + public bool Equals(PathString other) + { + return Equals(other, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Compares this PathString value to another value using a specific StringComparison type + /// + /// The second PathString for comparison + /// The StringComparison type to use + /// True if both PathString values are equal + public bool Equals(PathString other, StringComparison comparisonType) + { + if (!HasValue && !other.HasValue) + { + return true; + } + return string.Equals(_value, other._value, comparisonType); + } + + /// + /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. + /// + /// The second PathString for comparison. + /// True if both PathString values are equal + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return !HasValue; + } + return obj is PathString && Equals((PathString)obj); + } + + /// + /// Returns the hash code for the PathString value. The hash code is provided by the OrdinalIgnoreCase implementation. + /// + /// The hash code + public override int GetHashCode() + { + return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); + } + + /// + /// Operator call through to Equals + /// + /// The left parameter + /// The right parameter + /// True if both PathString values are equal + public static bool operator ==(PathString left, PathString right) + { + return left.Equals(right); + } + + /// + /// Operator call through to Equals + /// + /// The left parameter + /// The right parameter + /// True if both PathString values are not equal + public static bool operator !=(PathString left, PathString right) + { + return !left.Equals(right); + } + + /// + /// + /// The left parameter + /// The right parameter + /// The ToString combination of both values + public static string operator +(string left, PathString right) + { + // This overload exists to prevent the implicit string<->PathString converter from + // trying to call the PathString+PathString operator for things that are not path strings. + return string.Concat(left, right.ToString()); + } + + /// + /// + /// The left parameter + /// The right parameter + /// The ToString combination of both values + public static string operator +(PathString left, string right) + { + // This overload exists to prevent the implicit string<->PathString converter from + // trying to call the PathString+PathString operator for things that are not path strings. + return string.Concat(left.ToString(), right); + } + + /// + /// Operator call through to Add + /// + /// The left parameter + /// The right parameter + /// The PathString combination of both values + public static PathString operator +(PathString left, PathString right) + { + return left.Add(right); + } + + /// + /// Operator call through to Add + /// + /// The left parameter + /// The right parameter + /// The PathString combination of both values + public static string operator +(PathString left, QueryString right) + { + return left.Add(right); + } + + /// + /// Implicitly creates a new PathString from the given string. + /// + /// + public static implicit operator PathString(string s) + => ConvertFromString(s); + + /// + /// Implicitly calls ToString(). + /// + /// + public static implicit operator string(PathString path) + => path.ToString(); + + internal static PathString ConvertFromString(string s) + => string.IsNullOrEmpty(s) ? new PathString(s) : FromUriComponent(s); + } + + internal class PathStringConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + => sourceType == typeof(string) + ? true + : base.CanConvertFrom(context, sourceType); + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + => value is string + ? PathString.ConvertFromString((string)value) + : base.ConvertFrom(context, culture, value); + + public override object ConvertTo(ITypeDescriptorContext context, + CultureInfo culture, object value, Type destinationType) + => destinationType == typeof(string) + ? value.ToString() + : base.ConvertTo(context, culture, value, destinationType); + } +} diff --git a/src/Http/Http.Abstractions/src/Properties/AssemblyInfo.cs b/src/Http/Http.Abstractions/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2bdc2a912f --- /dev/null +++ b/src/Http/Http.Abstractions/src/Properties/AssemblyInfo.cs @@ -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.AspNetCore.Http.Abstractions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs b/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..6af7d138be --- /dev/null +++ b/src/Http/Http.Abstractions/src/Properties/Resources.Designer.cs @@ -0,0 +1,212 @@ +// +namespace Microsoft.AspNetCore.Http.Abstractions +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Http.Abstractions.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// '{0}' is not available. + /// + internal static string Exception_UseMiddlewareIServiceProviderNotAvailable + { + get => GetString("Exception_UseMiddlewareIServiceProviderNotAvailable"); + } + + /// + /// '{0}' is not available. + /// + internal static string FormatException_UseMiddlewareIServiceProviderNotAvailable(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareIServiceProviderNotAvailable"), p0); + + /// + /// No public '{0}' or '{1}' method found for middleware of type '{2}'. + /// + internal static string Exception_UseMiddlewareNoInvokeMethod + { + get => GetString("Exception_UseMiddlewareNoInvokeMethod"); + } + + /// + /// No public '{0}' or '{1}' method found for middleware of type '{2}'. + /// + internal static string FormatException_UseMiddlewareNoInvokeMethod(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareNoInvokeMethod"), p0, p1, p2); + + /// + /// '{0}' or '{1}' does not return an object of type '{2}'. + /// + internal static string Exception_UseMiddlewareNonTaskReturnType + { + get => GetString("Exception_UseMiddlewareNonTaskReturnType"); + } + + /// + /// '{0}' or '{1}' does not return an object of type '{2}'. + /// + internal static string FormatException_UseMiddlewareNonTaskReturnType(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareNonTaskReturnType"), p0, p1, p2); + + /// + /// The '{0}' or '{1}' method's first argument must be of type '{2}'. + /// + internal static string Exception_UseMiddlewareNoParameters + { + get => GetString("Exception_UseMiddlewareNoParameters"); + } + + /// + /// The '{0}' or '{1}' method's first argument must be of type '{2}'. + /// + internal static string FormatException_UseMiddlewareNoParameters(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareNoParameters"), p0, p1, p2); + + /// + /// Multiple public '{0}' or '{1}' methods are available. + /// + internal static string Exception_UseMiddleMutlipleInvokes + { + get => GetString("Exception_UseMiddleMutlipleInvokes"); + } + + /// + /// Multiple public '{0}' or '{1}' methods are available. + /// + internal static string FormatException_UseMiddleMutlipleInvokes(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddleMutlipleInvokes"), p0, p1); + + /// + /// The path in '{0}' must start with '/'. + /// + internal static string Exception_PathMustStartWithSlash + { + get => GetString("Exception_PathMustStartWithSlash"); + } + + /// + /// The path in '{0}' must start with '/'. + /// + internal static string FormatException_PathMustStartWithSlash(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_PathMustStartWithSlash"), p0); + + /// + /// Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'. + /// + internal static string Exception_InvokeMiddlewareNoService + { + get => GetString("Exception_InvokeMiddlewareNoService"); + } + + /// + /// Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'. + /// + internal static string FormatException_InvokeMiddlewareNoService(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_InvokeMiddlewareNoService"), p0, p1); + + /// + /// The '{0}' method must not have ref or out parameters. + /// + internal static string Exception_InvokeDoesNotSupportRefOrOutParams + { + get => GetString("Exception_InvokeDoesNotSupportRefOrOutParams"); + } + + /// + /// The '{0}' method must not have ref or out parameters. + /// + internal static string FormatException_InvokeDoesNotSupportRefOrOutParams(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_InvokeDoesNotSupportRefOrOutParams"), p0); + + /// + /// The value must be greater than zero. + /// + internal static string Exception_PortMustBeGreaterThanZero + { + get => GetString("Exception_PortMustBeGreaterThanZero"); + } + + /// + /// The value must be greater than zero. + /// + internal static string FormatException_PortMustBeGreaterThanZero() + => GetString("Exception_PortMustBeGreaterThanZero"); + + /// + /// No service for type '{0}' has been registered. + /// + internal static string Exception_UseMiddlewareNoMiddlewareFactory + { + get => GetString("Exception_UseMiddlewareNoMiddlewareFactory"); + } + + /// + /// No service for type '{0}' has been registered. + /// + internal static string FormatException_UseMiddlewareNoMiddlewareFactory(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareNoMiddlewareFactory"), p0); + + /// + /// '{0}' failed to create middleware of type '{1}'. + /// + internal static string Exception_UseMiddlewareUnableToCreateMiddleware + { + get => GetString("Exception_UseMiddlewareUnableToCreateMiddleware"); + } + + /// + /// '{0}' failed to create middleware of type '{1}'. + /// + internal static string FormatException_UseMiddlewareUnableToCreateMiddleware(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareUnableToCreateMiddleware"), p0, p1); + + /// + /// Types that implement '{0}' do not support explicit arguments. + /// + internal static string Exception_UseMiddlewareExplicitArgumentsNotSupported + { + get => GetString("Exception_UseMiddlewareExplicitArgumentsNotSupported"); + } + + /// + /// Types that implement '{0}' do not support explicit arguments. + /// + internal static string FormatException_UseMiddlewareExplicitArgumentsNotSupported(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddlewareExplicitArgumentsNotSupported"), p0); + + /// + /// Argument cannot be null or empty. + /// + internal static string ArgumentCannotBeNullOrEmpty + { + get => GetString("ArgumentCannotBeNullOrEmpty"); + } + + /// + /// Argument cannot be null or empty. + /// + internal static string FormatArgumentCannotBeNullOrEmpty() + => GetString("ArgumentCannotBeNullOrEmpty"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Http/Http.Abstractions/src/QueryString.cs b/src/Http/Http.Abstractions/src/QueryString.cs new file mode 100644 index 0000000000..772df8dfd9 --- /dev/null +++ b/src/Http/Http.Abstractions/src/QueryString.cs @@ -0,0 +1,261 @@ +// 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.Text; +using System.Text.Encodings.Web; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string + /// + public struct QueryString : IEquatable + { + /// + /// Represents the empty query string. This field is read-only. + /// + public static readonly QueryString Empty = new QueryString(string.Empty); + + private readonly string _value; + + /// + /// Initialize the query string with a given value. This value must be in escaped and delimited format with + /// a leading '?' character. + /// + /// The query string to be assigned to the Value property. + public QueryString(string value) + { + if (!string.IsNullOrEmpty(value) && value[0] != '?') + { + throw new ArgumentException("The leading '?' must be included for a non-empty query.", nameof(value)); + } + _value = value; + } + + /// + /// The escaped query string with the leading '?' character + /// + public string Value + { + get { return _value; } + } + + /// + /// True if the query string is not empty + /// + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } + + /// + /// Provides the query string escaped in a way which is correct for combining into the URI representation. + /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// + /// The query string value + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Provides the query string escaped in a way which is correct for combining into the URI representation. + /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// + /// The query string value + public string ToUriComponent() + { + // Escape things properly so System.Uri doesn't mis-interpret the data. + return HasValue ? _value.Replace("#", "%23") : string.Empty; + } + + /// + /// Returns an QueryString given the query as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a query. + /// + /// The escaped query as it appears in the URI format. + /// The resulting QueryString + public static QueryString FromUriComponent(string uriComponent) + { + if (string.IsNullOrEmpty(uriComponent)) + { + return new QueryString(string.Empty); + } + return new QueryString(uriComponent); + } + + /// + /// Returns an QueryString given the query as from a Uri object. Relative Uri objects are not supported. + /// + /// The Uri object + /// The resulting QueryString + public static QueryString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped); + if (!string.IsNullOrEmpty(queryValue)) + { + queryValue = "?" + queryValue; + } + return new QueryString(queryValue); + } + + /// + /// Create a query string with a single given parameter name and value. + /// + /// The un-encoded parameter name + /// The un-encoded parameter value + /// The resulting QueryString + public static QueryString Create(string name, string value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (!string.IsNullOrEmpty(value)) + { + value = UrlEncoder.Default.Encode(value); + } + return new QueryString($"?{UrlEncoder.Default.Encode(name)}={value}"); + } + + /// + /// Creates a query string composed from the given name value pairs. + /// + /// + /// The resulting QueryString + public static QueryString Create(IEnumerable> parameters) + { + var builder = new StringBuilder(); + bool first = true; + foreach (var pair in parameters) + { + AppendKeyValuePair(builder, pair.Key, pair.Value, first); + first = false; + } + + return new QueryString(builder.ToString()); + } + + /// + /// Creates a query string composed from the given name value pairs. + /// + /// + /// The resulting QueryString + public static QueryString Create(IEnumerable> parameters) + { + var builder = new StringBuilder(); + bool first = true; + + foreach (var pair in parameters) + { + // If nothing in this pair.Values, append null value and continue + if (StringValues.IsNullOrEmpty(pair.Value)) + { + AppendKeyValuePair(builder, pair.Key, null, first); + first = false; + continue; + } + // Otherwise, loop through values in pair.Value + foreach (var value in pair.Value) + { + AppendKeyValuePair(builder, pair.Key, value, first); + first = false; + } + } + + return new QueryString(builder.ToString()); + } + + public QueryString Add(QueryString other) + { + if (!HasValue || Value.Equals("?", StringComparison.Ordinal)) + { + return other; + } + if (!other.HasValue || other.Value.Equals("?", StringComparison.Ordinal)) + { + return this; + } + + // ?name1=value1 Add ?name2=value2 returns ?name1=value1&name2=value2 + return new QueryString(_value + "&" + other.Value.Substring(1)); + } + + public QueryString Add(string name, string value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (!HasValue || Value.Equals("?", StringComparison.Ordinal)) + { + return Create(name, value); + } + + var builder = new StringBuilder(Value); + AppendKeyValuePair(builder, name, value, first: false); + return new QueryString(builder.ToString()); + } + + public bool Equals(QueryString other) + { + if (!HasValue && !other.HasValue) + { + return true; + } + return string.Equals(_value, other._value, StringComparison.Ordinal); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return !HasValue; + } + return obj is QueryString && Equals((QueryString)obj); + } + + public override int GetHashCode() + { + return (HasValue ? _value.GetHashCode() : 0); + } + + public static bool operator ==(QueryString left, QueryString right) + { + return left.Equals(right); + } + + public static bool operator !=(QueryString left, QueryString right) + { + return !left.Equals(right); + } + + public static QueryString operator +(QueryString left, QueryString right) + { + return left.Add(right); + } + + private static void AppendKeyValuePair(StringBuilder builder, string key, string value, bool first) + { + builder.Append(first ? "?" : "&"); + builder.Append(UrlEncoder.Default.Encode(key)); + builder.Append("="); + if (!string.IsNullOrEmpty(value)) + { + builder.Append(UrlEncoder.Default.Encode(value)); + } + } + } +} diff --git a/src/Http/Http.Abstractions/src/RequestDelegate.cs b/src/Http/Http.Abstractions/src/RequestDelegate.cs new file mode 100644 index 0000000000..aecf353b29 --- /dev/null +++ b/src/Http/Http.Abstractions/src/RequestDelegate.cs @@ -0,0 +1,14 @@ +// 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.Http +{ + /// + /// A function that can process an HTTP request. + /// + /// The for the request. + /// A task that represents the completion of request processing. + public delegate Task RequestDelegate(HttpContext context); +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Resources.resx b/src/Http/Http.Abstractions/src/Resources.resx new file mode 100644 index 0000000000..dfdfeaf7d1 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Resources.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{0}' is not available. + + + No public '{0}' or '{1}' method found for middleware of type '{2}'. + + + '{0}' or '{1}' does not return an object of type '{2}'. + + + The '{0}' or '{1}' method's first argument must be of type '{2}'. + + + Multiple public '{0}' or '{1}' methods are available. + + + The path in '{0}' must start with '/'. + + + Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'. + + + The '{0}' method must not have ref or out parameters. + + + The value must be greater than zero. + + + No service for type '{0}' has been registered. + + + '{0}' failed to create middleware of type '{1}'. + + + Types that implement '{0}' do not support explicit arguments. + + + Argument cannot be null or empty. + + \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/StatusCodes.cs b/src/Http/Http.Abstractions/src/StatusCodes.cs new file mode 100644 index 0000000000..3261bce2f2 --- /dev/null +++ b/src/Http/Http.Abstractions/src/StatusCodes.cs @@ -0,0 +1,79 @@ +// 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 +{ + // Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + public static class StatusCodes + { + public const int Status100Continue = 100; + public const int Status101SwitchingProtocols = 101; + public const int Status102Processing = 102; + + public const int Status200OK = 200; + public const int Status201Created = 201; + public const int Status202Accepted = 202; + public const int Status203NonAuthoritative = 203; + public const int Status204NoContent = 204; + public const int Status205ResetContent = 205; + public const int Status206PartialContent = 206; + public const int Status207MultiStatus = 207; + public const int Status208AlreadyReported = 208; + public const int Status226IMUsed = 226; + + public const int Status300MultipleChoices = 300; + public const int Status301MovedPermanently = 301; + public const int Status302Found = 302; + public const int Status303SeeOther = 303; + public const int Status304NotModified = 304; + public const int Status305UseProxy = 305; + public const int Status306SwitchProxy = 306; // RFC 2616, removed + public const int Status307TemporaryRedirect = 307; + public const int Status308PermanentRedirect = 308; + + public const int Status400BadRequest = 400; + public const int Status401Unauthorized = 401; + public const int Status402PaymentRequired = 402; + public const int Status403Forbidden = 403; + public const int Status404NotFound = 404; + public const int Status405MethodNotAllowed = 405; + public const int Status406NotAcceptable = 406; + public const int Status407ProxyAuthenticationRequired = 407; + public const int Status408RequestTimeout = 408; + public const int Status409Conflict = 409; + public const int Status410Gone = 410; + public const int Status411LengthRequired = 411; + public const int Status412PreconditionFailed = 412; + public const int Status413RequestEntityTooLarge = 413; // RFC 2616, renamed + public const int Status413PayloadTooLarge = 413; // RFC 7231 + public const int Status414RequestUriTooLong = 414; // RFC 2616, renamed + public const int Status414UriTooLong = 414; // RFC 7231 + public const int Status415UnsupportedMediaType = 415; + public const int Status416RequestedRangeNotSatisfiable = 416; // RFC 2616, renamed + public const int Status416RangeNotSatisfiable = 416; // RFC 7233 + public const int Status417ExpectationFailed = 417; + public const int Status418ImATeapot = 418; + public const int Status419AuthenticationTimeout = 419; // Not defined in any RFC + public const int Status421MisdirectedRequest = 421; + public const int Status422UnprocessableEntity = 422; + public const int Status423Locked = 423; + public const int Status424FailedDependency = 424; + public const int Status426UpgradeRequired = 426; + public const int Status428PreconditionRequired = 428; + public const int Status429TooManyRequests = 429; + public const int Status431RequestHeaderFieldsTooLarge = 431; + public const int Status451UnavailableForLegalReasons = 451; + + public const int Status500InternalServerError = 500; + public const int Status501NotImplemented = 501; + public const int Status502BadGateway = 502; + public const int Status503ServiceUnavailable = 503; + public const int Status504GatewayTimeout = 504; + public const int Status505HttpVersionNotsupported = 505; + public const int Status506VariantAlsoNegotiates = 506; + public const int Status507InsufficientStorage = 507; + public const int Status508LoopDetected = 508; + public const int Status510NotExtended = 510; + public const int Status511NetworkAuthenticationRequired = 511; + } +} diff --git a/src/Http/Http.Abstractions/src/WebSocketManager.cs b/src/Http/Http.Abstractions/src/WebSocketManager.cs new file mode 100644 index 0000000000..79afefa5c0 --- /dev/null +++ b/src/Http/Http.Abstractions/src/WebSocketManager.cs @@ -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.Collections.Generic; +using System.Net.WebSockets; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Manages the establishment of WebSocket connections for a specific HTTP request. + /// + public abstract class WebSocketManager + { + /// + /// Gets a value indicating whether the request is a WebSocket establishment request. + /// + public abstract bool IsWebSocketRequest { get; } + + /// + /// Gets the list of requested WebSocket sub-protocols. + /// + public abstract IList WebSocketRequestedProtocols { get; } + + /// + /// Transitions the request to a WebSocket connection. + /// + /// A task representing the completion of the transition. + public virtual Task AcceptWebSocketAsync() + { + return AcceptWebSocketAsync(subProtocol: null); + } + + /// + /// Transitions the request to a WebSocket connection using the specified sub-protocol. + /// + /// The sub-protocol to use. + /// A task representing the completion of the transition. + public abstract Task AcceptWebSocketAsync(string subProtocol); + } +} diff --git a/src/Http/Http.Abstractions/src/baseline.netcore.json b/src/Http/Http.Abstractions/src/baseline.netcore.json new file mode 100644 index 0000000000..f407fb08e6 --- /dev/null +++ b/src/Http/Http.Abstractions/src/baseline.netcore.json @@ -0,0 +1,5020 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Http.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.MapExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Map", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "pathMatch", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "configuration", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.MapWhenExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "MapWhen", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "predicate", + "Type": "System.Func" + }, + { + "Name": "configuration", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.RunExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "handler", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.UseExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Use", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "middleware", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.UseMiddlewareExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseMiddleware", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "args", + "Type": "System.Object[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TMiddleware", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "UseMiddleware", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "middleware", + "Type": "System.Type" + }, + { + "Name": "args", + "Type": "System.Object[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.UsePathBaseExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UsePathBase", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "pathBase", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.UseWhenExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseWhen", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "predicate", + "Type": "System.Func" + }, + { + "Name": "configuration", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ApplicationServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ApplicationServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ServerFeatures", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Use", + "Parameters": [ + { + "Name": "middleware", + "Type": "System.Func" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "New", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.MapMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.Extensions.MapOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.MapOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_PathMatch", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PathMatch", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Branch", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Branch", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.MapWhenMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.Extensions.MapWhenOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.MapWhenOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Predicate", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Predicate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Branch", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Branch", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.Extensions.UsePathBaseMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "pathBase", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.ConnectionInfo", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Id", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Id", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemotePort", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemotePort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalPort", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalPort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientCertificate", + "Parameters": [], + "ReturnType": "System.Security.Cryptography.X509Certificates.X509Certificate2", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientCertificate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetClientCertificateAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Domain", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Domain", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SameSite", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.SameSiteMode", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SameSite", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.SameSiteMode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurePolicy", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurePolicy", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expiration", + "Parameters": [], + "ReturnType": "System.Nullable", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expiration", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsEssential", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsEssential", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "expiresFrom", + "Type": "System.DateTimeOffset" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.CookieSecurePolicy", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "SameAsRequest", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Always", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "None", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HeaderDictionaryExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendCommaSeparatedValues", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetCommaSeparatedValues", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String[]", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetCommaSeparatedValues", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpResponseWritingExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "text", + "Type": "System.String" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "text", + "Type": "System.String" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.FragmentString", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IEquatable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValue", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToUriComponent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uriComponent", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.FragmentString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.FragmentString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IEquatable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Equality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Inequality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.FragmentString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Empty", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.FragmentString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HostString", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IEquatable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValue", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Host", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Port", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToUriComponent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uriComponent", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.HostString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.HostString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MatchesAny", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "patterns", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IEquatable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Equality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.HostString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Inequality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.HostString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "host", + "Type": "System.String" + }, + { + "Name": "port", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Features", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Request", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Connection", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ConnectionInfo", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebSockets", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.WebSocketManager", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Authentication", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Items", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequestServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequestAborted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestAborted", + "Parameters": [ + { + "Name": "value", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TraceIdentifier", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TraceIdentifier", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Session", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ISession", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Session", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.ISession" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpMethods", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "IsConnect", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsDelete", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsGet", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsHead", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsOptions", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsPatch", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsPost", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsPut", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsTrace", + "Parameters": [ + { + "Name": "method", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Connect", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Delete", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Get", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Head", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Options", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Patch", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Post", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Put", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Trace", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpRequest", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Method", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Method", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Scheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsHttps", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsHttps", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Host", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HostString", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Host", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PathBase", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PathBase", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_QueryString", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_QueryString", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Query", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IQueryCollection", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Query", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IQueryCollection" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Protocol", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Protocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IRequestCookieCollection", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookies", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IRequestCookieCollection" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasFormContentType", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Form", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Form", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IFormCollection" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpResponse", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StatusCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StatusCode", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IResponseCookies", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasStarted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnCompleted", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RegisterForDispose", + "Parameters": [ + { + "Name": "disposable", + "Type": "System.IDisposable" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnCompleted", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Redirect", + "Parameters": [ + { + "Name": "location", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Redirect", + "Parameters": [ + { + "Name": "location", + "Type": "System.String" + }, + { + "Name": "permanent", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IHttpContextAccessor", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpContext", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IHttpContextFactory", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "featureCollection", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IMiddleware", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "InvokeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IMiddlewareFactory", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "middlewareType", + "Type": "System.Type" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.IMiddleware", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Release", + "Parameters": [ + { + "Name": "middleware", + "Type": "Microsoft.AspNetCore.Http.IMiddleware" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IEquatable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValue", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToUriComponent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uriComponent", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "comparisonType", + "Type": "System.StringComparison" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "remaining", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "comparisonType", + "Type": "System.StringComparison" + }, + { + "Name": "remaining", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "matched", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + }, + { + "Name": "remaining", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "StartsWithSegments", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "comparisonType", + "Type": "System.StringComparison" + }, + { + "Name": "matched", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + }, + { + "Name": "remaining", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IEquatable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "comparisonType", + "Type": "System.StringComparison" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Equality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Inequality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "System.String" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.PathString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Implicit", + "Parameters": [ + { + "Name": "s", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Implicit", + "Parameters": [ + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Empty", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.QueryString", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IEquatable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValue", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToUriComponent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uriComponent", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromUriComponent", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "parameters", + "Type": "System.Collections.Generic.IEnumerable>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "parameters", + "Type": "System.Collections.Generic.IEnumerable>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "other", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IEquatable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Equality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.QueryString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Inequality", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.QueryString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Addition", + "Parameters": [ + { + "Name": "left", + "Type": "Microsoft.AspNetCore.Http.QueryString" + }, + { + "Name": "right", + "Type": "Microsoft.AspNetCore.Http.QueryString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Empty", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.RequestDelegate", + "Visibility": "Public", + "Kind": "Class", + "Sealed": true, + "BaseType": "System.MulticastDelegate", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BeginInvoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "callback", + "Type": "System.AsyncCallback" + }, + { + "Name": "object", + "Type": "System.Object" + } + ], + "ReturnType": "System.IAsyncResult", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EndInvoke", + "Parameters": [ + { + "Name": "result", + "Type": "System.IAsyncResult" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "object", + "Type": "System.Object" + }, + { + "Name": "method", + "Type": "System.IntPtr" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.StatusCodes", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Status100Continue", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "100" + }, + { + "Kind": "Field", + "Name": "Status101SwitchingProtocols", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "101" + }, + { + "Kind": "Field", + "Name": "Status102Processing", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "102" + }, + { + "Kind": "Field", + "Name": "Status200OK", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "200" + }, + { + "Kind": "Field", + "Name": "Status201Created", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "201" + }, + { + "Kind": "Field", + "Name": "Status202Accepted", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "202" + }, + { + "Kind": "Field", + "Name": "Status203NonAuthoritative", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "203" + }, + { + "Kind": "Field", + "Name": "Status204NoContent", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "204" + }, + { + "Kind": "Field", + "Name": "Status205ResetContent", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "205" + }, + { + "Kind": "Field", + "Name": "Status206PartialContent", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "206" + }, + { + "Kind": "Field", + "Name": "Status207MultiStatus", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "207" + }, + { + "Kind": "Field", + "Name": "Status208AlreadyReported", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "208" + }, + { + "Kind": "Field", + "Name": "Status226IMUsed", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "226" + }, + { + "Kind": "Field", + "Name": "Status300MultipleChoices", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "300" + }, + { + "Kind": "Field", + "Name": "Status301MovedPermanently", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "301" + }, + { + "Kind": "Field", + "Name": "Status302Found", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "302" + }, + { + "Kind": "Field", + "Name": "Status303SeeOther", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "303" + }, + { + "Kind": "Field", + "Name": "Status304NotModified", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "304" + }, + { + "Kind": "Field", + "Name": "Status305UseProxy", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "305" + }, + { + "Kind": "Field", + "Name": "Status306SwitchProxy", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "306" + }, + { + "Kind": "Field", + "Name": "Status307TemporaryRedirect", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "307" + }, + { + "Kind": "Field", + "Name": "Status308PermanentRedirect", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "308" + }, + { + "Kind": "Field", + "Name": "Status400BadRequest", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "400" + }, + { + "Kind": "Field", + "Name": "Status401Unauthorized", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "401" + }, + { + "Kind": "Field", + "Name": "Status402PaymentRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "402" + }, + { + "Kind": "Field", + "Name": "Status403Forbidden", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "403" + }, + { + "Kind": "Field", + "Name": "Status404NotFound", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "404" + }, + { + "Kind": "Field", + "Name": "Status405MethodNotAllowed", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "405" + }, + { + "Kind": "Field", + "Name": "Status406NotAcceptable", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "406" + }, + { + "Kind": "Field", + "Name": "Status407ProxyAuthenticationRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "407" + }, + { + "Kind": "Field", + "Name": "Status408RequestTimeout", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "408" + }, + { + "Kind": "Field", + "Name": "Status409Conflict", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "409" + }, + { + "Kind": "Field", + "Name": "Status410Gone", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "410" + }, + { + "Kind": "Field", + "Name": "Status411LengthRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "411" + }, + { + "Kind": "Field", + "Name": "Status412PreconditionFailed", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "412" + }, + { + "Kind": "Field", + "Name": "Status413RequestEntityTooLarge", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "413" + }, + { + "Kind": "Field", + "Name": "Status413PayloadTooLarge", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "413" + }, + { + "Kind": "Field", + "Name": "Status414RequestUriTooLong", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "414" + }, + { + "Kind": "Field", + "Name": "Status414UriTooLong", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "414" + }, + { + "Kind": "Field", + "Name": "Status415UnsupportedMediaType", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "415" + }, + { + "Kind": "Field", + "Name": "Status416RequestedRangeNotSatisfiable", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "416" + }, + { + "Kind": "Field", + "Name": "Status416RangeNotSatisfiable", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "416" + }, + { + "Kind": "Field", + "Name": "Status417ExpectationFailed", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "417" + }, + { + "Kind": "Field", + "Name": "Status418ImATeapot", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "418" + }, + { + "Kind": "Field", + "Name": "Status419AuthenticationTimeout", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "419" + }, + { + "Kind": "Field", + "Name": "Status421MisdirectedRequest", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "421" + }, + { + "Kind": "Field", + "Name": "Status422UnprocessableEntity", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "422" + }, + { + "Kind": "Field", + "Name": "Status423Locked", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "423" + }, + { + "Kind": "Field", + "Name": "Status424FailedDependency", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "424" + }, + { + "Kind": "Field", + "Name": "Status426UpgradeRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "426" + }, + { + "Kind": "Field", + "Name": "Status428PreconditionRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "428" + }, + { + "Kind": "Field", + "Name": "Status429TooManyRequests", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "429" + }, + { + "Kind": "Field", + "Name": "Status431RequestHeaderFieldsTooLarge", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "431" + }, + { + "Kind": "Field", + "Name": "Status451UnavailableForLegalReasons", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "451" + }, + { + "Kind": "Field", + "Name": "Status500InternalServerError", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "500" + }, + { + "Kind": "Field", + "Name": "Status501NotImplemented", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "501" + }, + { + "Kind": "Field", + "Name": "Status502BadGateway", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "502" + }, + { + "Kind": "Field", + "Name": "Status503ServiceUnavailable", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "503" + }, + { + "Kind": "Field", + "Name": "Status504GatewayTimeout", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "504" + }, + { + "Kind": "Field", + "Name": "Status505HttpVersionNotsupported", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "505" + }, + { + "Kind": "Field", + "Name": "Status506VariantAlsoNegotiates", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "506" + }, + { + "Kind": "Field", + "Name": "Status507InsufficientStorage", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "507" + }, + { + "Kind": "Field", + "Name": "Status508LoopDetected", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "508" + }, + { + "Kind": "Field", + "Name": "Status510NotExtended", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "510" + }, + { + "Kind": "Field", + "Name": "Status511NetworkAuthenticationRequired", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "511" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.WebSocketManager", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsWebSocketRequest", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebSocketRequestedProtocols", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AcceptWebSocketAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AcceptWebSocketAsync", + "Parameters": [ + { + "Name": "subProtocol", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Authentication.AuthenticateInfo", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Principal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Description", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationDescription", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Description", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationDescription" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Authentication.AuthenticationDescription", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DisplayName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetAuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetAuthenticateInfoAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.AuthenticateContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + }, + { + "Name": "behavior", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeBehavior" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AutomaticScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Automatic\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsPersistent", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsPersistent", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RedirectUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RedirectUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IssuedUtc", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IssuedUtc", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpiresUtc", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ExpiresUtc", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowRefresh", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AllowRefresh", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/test/CookieBuilderTests.cs b/src/Http/Http.Abstractions/test/CookieBuilderTests.cs new file mode 100644 index 0000000000..dd540ccc1b --- /dev/null +++ b/src/Http/Http.Abstractions/test/CookieBuilderTests.cs @@ -0,0 +1,57 @@ +// 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.AspNetCore.Http.Abstractions.Tests +{ + public class CookieBuilderTests + { + [Theory] + [InlineData(CookieSecurePolicy.Always, false, true)] + [InlineData(CookieSecurePolicy.Always, true, true)] + [InlineData(CookieSecurePolicy.SameAsRequest, true, true)] + [InlineData(CookieSecurePolicy.SameAsRequest, false, false)] + [InlineData(CookieSecurePolicy.None, true, false)] + [InlineData(CookieSecurePolicy.None, false, false)] + public void ConfiguresSecurePolicy(CookieSecurePolicy policy, bool requestIsHttps, bool secure) + { + var builder = new CookieBuilder + { + SecurePolicy = policy + }; + var context = new DefaultHttpContext(); + context.Request.IsHttps = requestIsHttps; + var options = builder.Build(context); + + Assert.Equal(secure, options.Secure); + } + + [Fact] + public void ComputesExpiration() + { + Assert.Null(new CookieBuilder().Build(new DefaultHttpContext()).Expires); + + var now = DateTimeOffset.Now; + var options = new CookieBuilder { Expiration = TimeSpan.FromHours(1) }.Build(new DefaultHttpContext(), now); + Assert.Equal(now.AddHours(1), options.Expires); + } + + [Fact] + public void ComputesMaxAge() + { + Assert.Null(new CookieBuilder().Build(new DefaultHttpContext()).MaxAge); + + var now = TimeSpan.FromHours(1); + var options = new CookieBuilder { MaxAge = now }.Build(new DefaultHttpContext()); + Assert.Equal(now, options.MaxAge); + } + + [Fact] + public void CookieBuilderPreservesDefaultPath() + { + Assert.Equal(new CookieOptions().Path, new CookieBuilder().Build(new DefaultHttpContext()).Path); + } + } +} diff --git a/src/Http/Http.Abstractions/test/FragmentStringTests.cs b/src/Http/Http.Abstractions/test/FragmentStringTests.cs new file mode 100644 index 0000000000..4f5fe20916 --- /dev/null +++ b/src/Http/Http.Abstractions/test/FragmentStringTests.cs @@ -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 Xunit; + +namespace Microsoft.AspNetCore.Http.Abstractions.Tests +{ + public class FragmentStringTests + { + [Fact] + public void Equals_EmptyFragmentStringAndDefaultFragmentString() + { + // Act and Assert + Assert.Equal(default(FragmentString), FragmentString.Empty); + Assert.Equal(default(FragmentString), FragmentString.Empty); + // explicitly checking == operator + Assert.True(FragmentString.Empty == default(FragmentString)); + Assert.True(default(FragmentString) == FragmentString.Empty); + } + + [Fact] + public void NotEquals_DefaultFragmentStringAndNonNullFragmentString() + { + // Arrange + var fragmentString = new FragmentString("#col=1"); + + // Act and Assert + Assert.NotEqual(default(FragmentString), fragmentString); + } + + [Fact] + public void NotEquals_EmptyFragmentStringAndNonNullFragmentString() + { + // Arrange + var fragmentString = new FragmentString("#col=1"); + + // Act and Assert + Assert.NotEqual(fragmentString, FragmentString.Empty); + } + } +} diff --git a/src/Http/Http.Abstractions/test/HostStringTest.cs b/src/Http/Http.Abstractions/test/HostStringTest.cs new file mode 100644 index 0000000000..85820f8ffc --- /dev/null +++ b/src/Http/Http.Abstractions/test/HostStringTest.cs @@ -0,0 +1,175 @@ +// 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.Testing; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HostStringTests + { + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void CtorThrows_IfPortIsNotGreaterThanZero(int port) + { + // Act and Assert + ExceptionAssert.ThrowsArgumentOutOfRange(() => new HostString("localhost", port), "port", "The value must be greater than zero."); + } + + [Theory] + [InlineData("localhost", "localhost")] + [InlineData("1.2.3.4", "1.2.3.4")] + [InlineData("[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]")] + [InlineData("本地主機", "本地主機")] + [InlineData("localhost:5000", "localhost")] + [InlineData("1.2.3.4:5000", "1.2.3.4")] + [InlineData("[2001:db8:a0b:12f0::1]:5000", "[2001:db8:a0b:12f0::1]")] + [InlineData("本地主機:5000", "本地主機")] + public void Domain_ExtractsHostFromValue(string sourceValue, string expectedDomain) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Host; + + // Assert + Assert.Equal(expectedDomain, result); + } + + [Theory] + [InlineData("localhost", null)] + [InlineData("1.2.3.4", null)] + [InlineData("[2001:db8:a0b:12f0::1]", null)] + [InlineData("本地主機", null)] + [InlineData("localhost:5000", 5000)] + [InlineData("1.2.3.4:5000", 5000)] + [InlineData("[2001:db8:a0b:12f0::1]:5000", 5000)] + [InlineData("本地主機:5000", 5000)] + public void Port_ExtractsPortFromValue(string sourceValue, int? expectedPort) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Port; + + // Assert + Assert.Equal(expectedPort, result); + } + + [Theory] + [InlineData("localhost:BLAH")] + public void Port_ExtractsInvalidPortFromValue(string sourceValue) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Port; + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("localhost", 5000, "localhost", 5000)] + [InlineData("1.2.3.4", 5000, "1.2.3.4", 5000)] + [InlineData("[2001:db8:a0b:12f0::1]", 5000, "[2001:db8:a0b:12f0::1]", 5000)] + [InlineData("2001:db8:a0b:12f0::1", 5000, "[2001:db8:a0b:12f0::1]", 5000)] + [InlineData("本地主機", 5000, "本地主機", 5000)] + public void Ctor_CreatesFromHostAndPort(string sourceHost, int sourcePort, string expectedHost, int expectedPort) + { + // Arrange + var hostString = new HostString(sourceHost, sourcePort); + + // Act + var host = hostString.Host; + var port = hostString.Port; + + // Assert + Assert.Equal(expectedHost, host); + Assert.Equal(expectedPort, port); + } + + [Fact] + public void Equals_EmptyHostStringAndDefaultHostString() + { + // Act and Assert + Assert.Equal(default(HostString), new HostString(string.Empty)); + Assert.Equal(default(HostString), new HostString(string.Empty)); + // explicitly checking == operator + Assert.True(new HostString(string.Empty) == default(HostString)); + Assert.True(default(HostString) == new HostString(string.Empty)); + } + + [Fact] + public void NotEquals_DefaultHostStringAndNonNullHostString() + { + // Arrange + var hostString = new HostString("example.com"); + + // Act and Assert + Assert.NotEqual(default(HostString), hostString); + } + + [Fact] + public void NotEquals_EmptyHostStringAndNonNullHostString() + { + // Arrange + var hostString = new HostString("example.com"); + + // Act and Assert + Assert.NotEqual(hostString, new HostString(string.Empty)); + } + + [Theory] + [InlineData("localHost", "localhost")] + [InlineData("localHost", "*")] // Any - Used by HttpSys + [InlineData("localhost:9090", "localHost")] + [InlineData("example.com:443", "example.com")] + [InlineData("foo.eXample.com:443", "*.exampLe.com")] + [InlineData("f.eXample.com:443", "*.exampLe.com")] + [InlineData("a.b.c.eXample.com:443", "*.exampLe.com")] + [InlineData("127.0.0.1", "127.0.0.1")] + [InlineData("127.0.0.1:443", "127.0.0.1")] + [InlineData("xn--c1yn36f:443", "xn--c1yn36f")] + [InlineData("點看", "點看")] + [InlineData("[::ABC]", "[::aBc]")] + [InlineData("[::1]:80", "[::1]")] + [InlineData("[::1]:", "[::1]")] + [InlineData("::1", "[::1]")] + public void HostMatches(string host, string pattern) + { + Assert.True(HostString.MatchesAny(host, new StringSegment[] { pattern })); + } + + [Theory] + [InlineData("example.com", "localhost")] + [InlineData("localhost:9090", "example.com")] + [InlineData(":80", "localhost")] + [InlineData(":", "localhost")] + [InlineData("example.com:443", "*.example.com")] + [InlineData(".example.com:443", "*.example.com")] + [InlineData("foo.com:443", "*.example.com")] + [InlineData("foo.example.com.bar:443", "*.example.com")] + [InlineData(".com:443", "*.com")] + [InlineData("xn--c1yn36f:443", "點看")] + [InlineData("[::1", "[::1]")] + [InlineData("[::1:80", "[::1]")] + [InlineData("::1", "::1")] // Brackets are added to the host before the comparison + public void HostDoesntMatch(string host, string pattern) + { + Assert.False(HostString.MatchesAny(host, new StringSegment[] { pattern })); + } + + [Fact] + public void HostMatchThrowsForBadPort() + { + Assert.Throws(() => HostString.MatchesAny("example.com:1abc", new StringSegment[] { "example.com" })); + } + } +} diff --git a/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs b/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs new file mode 100644 index 0000000000..f8e9e27d1c --- /dev/null +++ b/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs @@ -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.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HttpResponseWritingExtensionsTests + { + [Fact] + public async Task WritingText_WriteText() + { + HttpContext context = CreateRequest(); + await context.Response.WriteAsync("Hello World"); + + Assert.Equal(11, context.Response.Body.Length); + } + + [Fact] + public async Task WritingText_MultipleWrites() + { + HttpContext context = CreateRequest(); + await context.Response.WriteAsync("Hello World"); + await context.Response.WriteAsync("Hello World"); + + Assert.Equal(22, context.Response.Body.Length); + } + + private HttpContext CreateRequest() + { + HttpContext context = new DefaultHttpContext(); + context.Response.Body = new MemoryStream(); + return context; + } + } +} diff --git a/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.cs b/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.cs new file mode 100644 index 0000000000..a30e99603c --- /dev/null +++ b/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.cs @@ -0,0 +1,199 @@ +// 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.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + public class MapPathMiddlewareTests + { + private static readonly Action ActionNotImplemented = new Action(_ => { throw new NotImplementedException(); }); + + private static Task Success(HttpContext context) + { + context.Response.StatusCode = 200; + context.Items["test.PathBase"] = context.Request.PathBase.Value; + context.Items["test.Path"] = context.Request.Path.Value; + return Task.FromResult(null); + } + + private static void UseSuccess(IApplicationBuilder app) + { + app.Run(Success); + } + + private static Task NotImplemented(HttpContext context) + { + throw new NotImplementedException(); + } + + private static void UseNotImplemented(IApplicationBuilder app) + { + app.Run(NotImplemented); + } + + [Fact] + public void NullArguments_ArgumentNullException() + { + var builder = new ApplicationBuilder(serviceProvider: null); + var noMiddleware = new ApplicationBuilder(serviceProvider: null).Build(); + var noOptions = new MapOptions(); + Assert.Throws(() => builder.Map("/foo", configuration: null)); + Assert.Throws(() => new MapMiddleware(noMiddleware, null)); + } + + [Theory] + [InlineData("/foo", "", "/foo")] + [InlineData("/foo", "", "/foo/")] + [InlineData("/foo", "/Bar", "/foo")] + [InlineData("/foo", "/Bar", "/foo/cho")] + [InlineData("/foo", "/Bar", "/foo/cho/")] + [InlineData("/foo/cho", "/Bar", "/foo/cho")] + [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] + public void PathMatchFunc_BranchTaken(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map(matchPath, UseSuccess); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, context.Request.PathBase.Value); + Assert.Equal(requestPath, context.Request.Path.Value); + } + + [Theory] + [InlineData("/foo", "", "/foo")] + [InlineData("/foo", "", "/foo/")] + [InlineData("/foo", "/Bar", "/foo")] + [InlineData("/foo", "/Bar", "/foo/cho")] + [InlineData("/foo", "/Bar", "/foo/cho/")] + [InlineData("/foo/cho", "/Bar", "/foo/cho")] + [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] + [InlineData("/foo", "", "/Foo")] + [InlineData("/foo", "", "/Foo/")] + [InlineData("/foo", "/Bar", "/Foo")] + [InlineData("/foo", "/Bar", "/Foo/Cho")] + [InlineData("/foo", "/Bar", "/Foo/Cho/")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho/do")] + public void PathMatchAction_BranchTaken(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map(matchPath, subBuilder => subBuilder.Run(Success)); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath + requestPath.Substring(0, matchPath.Length), (string)context.Items["test.PathBase"]); + Assert.Equal(requestPath.Substring(matchPath.Length), context.Items["test.Path"]); + } + + [Theory] + [InlineData("/")] + [InlineData("/foo/")] + [InlineData("/foo/cho/")] + public void MatchPathWithTrailingSlashThrowsException(string matchPath) + { + Assert.Throws(() => new ApplicationBuilder(serviceProvider: null).Map(matchPath, map => { }).Build()); + } + + [Theory] + [InlineData("/foo", "", "")] + [InlineData("/foo", "/bar", "")] + [InlineData("/foo", "", "/bar")] + [InlineData("/foo", "/foo", "")] + [InlineData("/foo", "/foo", "/bar")] + [InlineData("/foo", "", "/bar/foo")] + [InlineData("/foo/bar", "/foo", "/bar")] + public void PathMismatchFunc_PassedThrough(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map(matchPath, UseNotImplemented); + builder.Run(Success); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, context.Request.PathBase.Value); + Assert.Equal(requestPath, context.Request.Path.Value); + } + + [Theory] + [InlineData("/foo", "", "")] + [InlineData("/foo", "/bar", "")] + [InlineData("/foo", "", "/bar")] + [InlineData("/foo", "/foo", "")] + [InlineData("/foo", "/foo", "/bar")] + [InlineData("/foo", "", "/bar/foo")] + [InlineData("/foo/bar", "/foo", "/bar")] + public void PathMismatchAction_PassedThrough(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map(matchPath, UseNotImplemented); + builder.Run(Success); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, context.Request.PathBase.Value); + Assert.Equal(requestPath, context.Request.Path.Value); + } + + [Fact] + public void ChainedRoutes_Success() + { + var builder = new ApplicationBuilder(serviceProvider: null); + builder.Map("/route1", map => + { + map.Map("/subroute1", UseSuccess); + map.Run(NotImplemented); + }); + builder.Map("/route2/subroute2", UseSuccess); + var app = builder.Build(); + + HttpContext context = CreateRequest(string.Empty, "/route1"); + Assert.Throws(() => app.Invoke(context).Wait()); + + context = CreateRequest(string.Empty, "/route1/subroute1"); + app.Invoke(context).Wait(); + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route1/subroute1", context.Request.Path.Value); + + context = CreateRequest(string.Empty, "/route2"); + app.Invoke(context).Wait(); + Assert.Equal(404, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route2", context.Request.Path.Value); + + context = CreateRequest(string.Empty, "/route2/subroute2"); + app.Invoke(context).Wait(); + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route2/subroute2", context.Request.Path.Value); + + context = CreateRequest(string.Empty, "/route2/subroute2/subsub2"); + app.Invoke(context).Wait(); + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route2/subroute2/subsub2", context.Request.Path.Value); + } + + private HttpContext CreateRequest(string basePath, string requestPath) + { + HttpContext context = new DefaultHttpContext(); + context.Request.PathBase = new PathString(basePath); + context.Request.Path = new PathString(requestPath); + return context; + } + } +} diff --git a/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs b/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs new file mode 100644 index 0000000000..0313a730d5 --- /dev/null +++ b/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs @@ -0,0 +1,123 @@ +// 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.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + using Predicate = Func; + + public class MapPredicateMiddlewareTests + { + private static readonly Predicate NotImplementedPredicate = new Predicate(envionment => { throw new NotImplementedException(); }); + + private static Task Success(HttpContext context) + { + context.Response.StatusCode = 200; + return Task.FromResult(null); + } + + private static void UseSuccess(IApplicationBuilder app) + { + app.Run(Success); + } + + private static Task NotImplemented(HttpContext context) + { + throw new NotImplementedException(); + } + + private static void UseNotImplemented(IApplicationBuilder app) + { + app.Run(NotImplemented); + } + + private bool TruePredicate(HttpContext context) + { + return true; + } + + private bool FalsePredicate(HttpContext context) + { + return false; + } + + [Fact] + public void NullArguments_ArgumentNullException() + { + var builder = new ApplicationBuilder(serviceProvider: null); + var noMiddleware = new ApplicationBuilder(serviceProvider: null).Build(); + var noOptions = new MapWhenOptions(); + Assert.Throws(() => builder.MapWhen(null, UseNotImplemented)); + Assert.Throws(() => builder.MapWhen(NotImplementedPredicate, configuration: null)); + Assert.Throws(() => new MapWhenMiddleware(null, noOptions)); + Assert.Throws(() => new MapWhenMiddleware(noMiddleware, null)); + Assert.Throws(() => new MapWhenMiddleware(null, noOptions)); + Assert.Throws(() => new MapWhenMiddleware(noMiddleware, null)); + } + + [Fact] + public void PredicateTrue_BranchTaken() + { + HttpContext context = CreateRequest(); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.MapWhen(TruePredicate, UseSuccess); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + } + + [Fact] + public void PredicateTrueAction_BranchTaken() + { + HttpContext context = CreateRequest(); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.MapWhen(TruePredicate, UseSuccess); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + } + + [Fact] + public void PredicateFalseAction_PassThrough() + { + HttpContext context = CreateRequest(); + var builder = new ApplicationBuilder(serviceProvider: null); + builder.MapWhen(FalsePredicate, UseNotImplemented); + builder.Run(Success); + var app = builder.Build(); + app.Invoke(context).Wait(); + + Assert.Equal(200, context.Response.StatusCode); + } + + [Fact] + public void ChainedPredicates_Success() + { + var builder = new ApplicationBuilder(serviceProvider: null); + builder.MapWhen(TruePredicate, map1 => + { + map1.MapWhen((Predicate)FalsePredicate, UseNotImplemented); + map1.MapWhen((Predicate)TruePredicate, map2 => map2.MapWhen((Predicate)TruePredicate, UseSuccess)); + map1.Run(NotImplemented); + }); + var app = builder.Build(); + + HttpContext context = CreateRequest(); + app.Invoke(context).Wait(); + Assert.Equal(200, context.Response.StatusCode); + } + + private HttpContext CreateRequest() + { + HttpContext context = new DefaultHttpContext(); + return context; + } + } +} diff --git a/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj b/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj new file mode 100644 index 0000000000..a97c164925 --- /dev/null +++ b/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj @@ -0,0 +1,11 @@ + + + + $(StandardTestTfms) + + + + + + + diff --git a/src/Http/Http.Abstractions/test/PathStringTests.cs b/src/Http/Http.Abstractions/test/PathStringTests.cs new file mode 100644 index 0000000000..2d3c6e23f0 --- /dev/null +++ b/src/Http/Http.Abstractions/test/PathStringTests.cs @@ -0,0 +1,240 @@ +// 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.ComponentModel; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class PathStringTests + { + [Fact] + public void CtorThrows_IfPathDoesNotHaveLeadingSlash() + { + // Act and Assert + ExceptionAssert.ThrowsArgument(() => new PathString("hello"), "value", "The path in 'value' must start with '/'."); + } + + [Fact] + public void Equals_EmptyPathStringAndDefaultPathString() + { + // Act and Assert + Assert.Equal(default(PathString), PathString.Empty); + Assert.Equal(default(PathString), PathString.Empty); + Assert.True(PathString.Empty == default(PathString)); + Assert.True(default(PathString) == PathString.Empty); + Assert.True(PathString.Empty.Equals(default(PathString))); + Assert.True(default(PathString).Equals(PathString.Empty)); + } + + [Fact] + public void NotEquals_DefaultPathStringAndNonNullPathString() + { + // Arrange + var pathString = new PathString("/hello"); + + // Act and Assert + Assert.NotEqual(default(PathString), pathString); + } + + [Fact] + public void NotEquals_EmptyPathStringAndNonNullPathString() + { + // Arrange + var pathString = new PathString("/hello"); + + // Act and Assert + Assert.NotEqual(pathString, PathString.Empty); + } + + [Fact] + public void HashCode_CheckNullAndEmptyHaveSameHashcodes() + { + Assert.Equal(PathString.Empty.GetHashCode(), default(PathString).GetHashCode()); + } + + [Theory] + [InlineData(null, null)] + [InlineData("", null)] + public void AddPathString_HandlesNullAndEmptyStrings(string appString, string concatString) + { + // Arrange + var appPath = new PathString(appString); + var concatPath = new PathString(concatString); + + // Act + var result = appPath.Add(concatPath); + + // Assert + Assert.False(result.HasValue); + } + + [Theory] + [InlineData("", "/", "/")] + [InlineData("/", null, "/")] + [InlineData("/", "", "/")] + [InlineData("/", "/test", "/test")] + [InlineData("/myapp/", "/test/bar", "/myapp/test/bar")] + [InlineData("/myapp/", "/test/bar/", "/myapp/test/bar/")] + public void AddPathString_HandlesLeadingAndTrailingSlashes(string appString, string concatString, string expected) + { + // Arrange + var appPath = new PathString(appString); + var concatPath = new PathString(concatString); + + // Act + var result = appPath.Add(concatPath); + + // Assert + Assert.Equal(expected, result.Value); + } + + [Fact] + public void ImplicitStringConverters_WorksWithAdd() + { + var scheme = "http"; + var host = new HostString("localhost:80"); + var pathBase = new PathString("/base"); + var path = new PathString("/path"); + var query = new QueryString("?query"); + var fragment = new FragmentString("#frag"); + + var result = scheme + "://" + host + pathBase + path + query + fragment; + Assert.Equal("http://localhost:80/base/path?query#frag", result); + + result = pathBase + path + query + fragment; + Assert.Equal("/base/path?query#frag", result); + + result = path + "text"; + Assert.Equal("/pathtext", result); + } + + [Theory] + [InlineData("/test/path", "/TEST", true)] + [InlineData("/test/path", "/TEST/pa", false)] + [InlineData("/TEST/PATH", "/test", true)] + [InlineData("/TEST/path", "/test/pa", false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", true)] + public void StartsWithSegments_DoesACaseInsensitiveMatch(string sourcePath, string testPath, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); + + var result = source.StartsWithSegments(test); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("/test/path", "/TEST", true)] + [InlineData("/test/path", "/TEST/pa", false)] + [InlineData("/TEST/PATH", "/test", true)] + [InlineData("/TEST/path", "/test/pa", false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", true)] + public void StartsWithSegmentsWithRemainder_DoesACaseInsensitiveMatch(string sourcePath, string testPath, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); + + var result = source.StartsWithSegments(test, out var remaining); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("/test/path", "/TEST", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/path", "/TEST", StringComparison.Ordinal, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.Ordinal, false)] + [InlineData("/TEST/PATH", "/test", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/TEST/PATH", "/test", StringComparison.Ordinal, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.Ordinal, false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.Ordinal, false)] + public void StartsWithSegments_DoesMatchUsingSpecifiedComparison(string sourcePath, string testPath, StringComparison comparison, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); + + var result = source.StartsWithSegments(test, comparison); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("/test/path", "/TEST", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/path", "/TEST", StringComparison.Ordinal, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.Ordinal, false)] + [InlineData("/TEST/PATH", "/test", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/TEST/PATH", "/test", StringComparison.Ordinal, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.Ordinal, false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.Ordinal, false)] + public void StartsWithSegmentsWithRemainder_DoesMatchUsingSpecifiedComparison(string sourcePath, string testPath, StringComparison comparison, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); + + var result = source.StartsWithSegments(test, comparison, out var remaining); + + Assert.Equal(expectedResult, result); + } + + [Theory] + // unreserved + [InlineData("/abc123.-_~", "/abc123.-_~")] + // colon + [InlineData("/:", "/:")] + // at + [InlineData("/@", "/@")] + // sub-delims + [InlineData("/!$&'()*+,;=", "/!$&'()*+,;=")] + // reserved + [InlineData("/?#[]", "/%3F%23%5B%5D")] + // pct-encoding + [InlineData("/单行道", "/%E5%8D%95%E8%A1%8C%E9%81%93")] + // mixed + [InlineData("/index/单行道=(x*y)[abc]", "/index/%E5%8D%95%E8%A1%8C%E9%81%93=(x*y)%5Babc%5D")] + [InlineData("/index/单行道=(x*y)[abc]_", "/index/%E5%8D%95%E8%A1%8C%E9%81%93=(x*y)%5Babc%5D_")] + // encoded + [InlineData("/http%3a%2f%2f[foo]%3A5000/", "/http%3a%2f%2f%5Bfoo%5D%3A5000/")] + [InlineData("/http%3a%2f%2f[foo]%3A5000/%", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%25")] + [InlineData("/http%3a%2f%2f[foo]%3A5000/%2", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%252")] + [InlineData("/http%3a%2f%2f[foo]%3A5000/%2F", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%2F")] + public void ToUriComponentEscapeCorrectly(string input, string expected) + { + var path = new PathString(input); + + Assert.Equal(expected, path.ToUriComponent()); + } + + [Fact] + public void PathStringConvertsOnlyToAndFromString() + { + var converter = TypeDescriptor.GetConverter(typeof(PathString)); + PathString result = (PathString)converter.ConvertFromInvariantString("/foo"); + Assert.Equal("/foo", result.ToString()); + Assert.Equal("/foo", converter.ConvertTo(result, typeof(string))); + Assert.True(converter.CanConvertFrom(typeof(string))); + Assert.False(converter.CanConvertFrom(typeof(int))); + Assert.False(converter.CanConvertFrom(typeof(bool))); + Assert.True(converter.CanConvertTo(typeof(string))); + Assert.False(converter.CanConvertTo(typeof(int))); + Assert.False(converter.CanConvertTo(typeof(bool))); + } + + [Fact] + public void PathStringStaysEqualAfterAssignments() + { + PathString p1 = "/?"; + string s1 = p1; + PathString p2 = s1; + Assert.Equal(p1, p2); + } + } +} diff --git a/src/Http/Http.Abstractions/test/QueryStringTests.cs b/src/Http/Http.Abstractions/test/QueryStringTests.cs new file mode 100644 index 0000000000..8327f12509 --- /dev/null +++ b/src/Http/Http.Abstractions/test/QueryStringTests.cs @@ -0,0 +1,166 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Abstractions +{ + public class QueryStringTests + { + [Fact] + public void CtorThrows_IfQueryDoesNotHaveLeadingQuestionMark() + { + // Act and Assert + ExceptionAssert.ThrowsArgument(() => new QueryString("hello"), "value", "The leading '?' must be included for a non-empty query."); + } + + [Fact] + public void CtorNullOrEmpty_Success() + { + var query = new QueryString(); + Assert.False(query.HasValue); + Assert.Null(query.Value); + + query = new QueryString(null); + Assert.False(query.HasValue); + Assert.Null(query.Value); + + query = new QueryString(string.Empty); + Assert.False(query.HasValue); + Assert.Equal(string.Empty, query.Value); + } + + [Fact] + public void CtorJustAQuestionMark_Success() + { + var query = new QueryString("?"); + Assert.True(query.HasValue); + Assert.Equal("?", query.Value); + } + + [Fact] + public void ToString_EncodesHash() + { + var query = new QueryString("?Hello=Wor#ld"); + Assert.Equal("?Hello=Wor%23ld", query.ToString()); + } + + [Theory] + [InlineData("name", "value", "?name=value")] + [InlineData("na me", "val ue", "?na%20me=val%20ue")] + [InlineData("name", "", "?name=")] + [InlineData("name", null, "?name=")] + [InlineData("", "value", "?=value")] + [InlineData("", "", "?=")] + [InlineData("", null, "?=")] + public void CreateNameValue_Success(string name, string value, string exepcted) + { + var query = QueryString.Create(name, value); + Assert.Equal(exepcted, query.Value); + } + + [Fact] + public void CreateFromList_Success() + { + var query = QueryString.Create(new[] + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2"), + new KeyValuePair("key3", "value3"), + new KeyValuePair("key4", null), + new KeyValuePair("key5", "") + }); + Assert.Equal("?key1=value1&key2=value2&key3=value3&key4=&key5=", query.Value); + } + + [Fact] + public void CreateFromListStringValues_Success() + { + var query = QueryString.Create(new[] + { + new KeyValuePair("key1", new StringValues("value1")), + new KeyValuePair("key2", new StringValues("value2")), + new KeyValuePair("key3", new StringValues("value3")), + new KeyValuePair("key4", new StringValues()), + new KeyValuePair("key5", new StringValues("")), + }); + Assert.Equal("?key1=value1&key2=value2&key3=value3&key4=&key5=", query.Value); + } + + [Theory] + [InlineData(null, null, null)] + [InlineData("", "", "")] + [InlineData(null, "?name2=value2", "?name2=value2")] + [InlineData("", "?name2=value2", "?name2=value2")] + [InlineData("?", "?name2=value2", "?name2=value2")] + [InlineData("?name1=value1", null, "?name1=value1")] + [InlineData("?name1=value1", "", "?name1=value1")] + [InlineData("?name1=value1", "?", "?name1=value1")] + [InlineData("?name1=value1", "?name2=value2", "?name1=value1&name2=value2")] + public void AddQueryString_Success(string query1, string query2, string expected) + { + var q1 = new QueryString(query1); + var q2 = new QueryString(query2); + Assert.Equal(expected, q1.Add(q2).Value); + Assert.Equal(expected, (q1 + q2).Value); + } + + [Theory] + [InlineData("", "", "", "?=")] + [InlineData("", "", null, "?=")] + [InlineData("?", "", "", "?=")] + [InlineData("?", "", null, "?=")] + [InlineData("?", "name2", "value2", "?name2=value2")] + [InlineData("?", "name2", "", "?name2=")] + [InlineData("?", "name2", null, "?name2=")] + [InlineData("?name1=value1", "name2", "value2", "?name1=value1&name2=value2")] + [InlineData("?name1=value1", "na me2", "val ue2", "?name1=value1&na%20me2=val%20ue2")] + [InlineData("?name1=value1", "", "", "?name1=value1&=")] + [InlineData("?name1=value1", "", null, "?name1=value1&=")] + [InlineData("?name1=value1", "name2", "", "?name1=value1&name2=")] + [InlineData("?name1=value1", "name2", null, "?name1=value1&name2=")] + public void AddNameValue_Success(string query1, string name2, string value2, string expected) + { + var q1 = new QueryString(query1); + var q2 = q1.Add(name2, value2); + Assert.Equal(expected, q2.Value); + } + + [Fact] + public void Equals_EmptyQueryStringAndDefaultQueryString() + { + // Act and Assert + Assert.Equal(default(QueryString), QueryString.Empty); + Assert.Equal(default(QueryString), QueryString.Empty); + // explicitly checking == operator + Assert.True(QueryString.Empty == default(QueryString)); + Assert.True(default(QueryString) == QueryString.Empty); + } + + [Fact] + public void NotEquals_DefaultQueryStringAndNonNullQueryString() + { + // Arrange + var queryString = new QueryString("?foo=1"); + + // Act and Assert + Assert.NotEqual(default(QueryString), queryString); + } + + [Fact] + public void NotEquals_EmptyQueryStringAndNonNullQueryString() + { + // Arrange + var queryString = new QueryString("?foo=1"); + + // Act and Assert + Assert.NotEqual(queryString, QueryString.Empty); + } + } +} diff --git a/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs b/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs new file mode 100644 index 0000000000..07c1aa4e8d --- /dev/null +++ b/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs @@ -0,0 +1,376 @@ +// 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.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http.Abstractions; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class UseMiddlewareTest + { + [Fact] + public void UseMiddleware_WithNoParameters_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareNoParametersStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNoParameters( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(HttpContext)), + exception.Message); + } + + [Fact] + public void UseMiddleware_AsyncWithNoParameters_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareAsyncNoParametersStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNoParameters( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(HttpContext)), + exception.Message); + } + + [Fact] + public void UseMiddleware_NonTaskReturnType_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareNonTaskReturnStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNonTaskReturnType( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(Task)), + exception.Message); + } + + [Fact] + public void UseMiddleware_AsyncNonTaskReturnType_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareAsyncNonTaskReturnStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNonTaskReturnType( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(Task)), + exception.Message); + } + + [Fact] + public void UseMiddleware_NoInvokeOrInvokeAsyncMethod_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareNoInvokeStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNoInvokeMethod( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, typeof(MiddlewareNoInvokeStub)), + exception.Message); + } + + [Fact] + public void UseMiddleware_MutlipleInvokeMethods_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareMultipleInvokesStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddleMutlipleInvokes( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName), + exception.Message); + } + + [Fact] + public void UseMiddleware_MutlipleInvokeAsyncMethods_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAsyncStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddleMutlipleInvokes( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName), + exception.Message); + } + + [Fact] + public void UseMiddleware_MutlipleInvokeAndInvokeAsyncMethods_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAndInvokeAsyncStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddleMutlipleInvokes( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName), + exception.Message); + } + + [Fact] + public async Task UseMiddleware_ThrowsIfArgCantBeResolvedFromContainer() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareInjectInvokeNoService)); + var app = builder.Build(); + var exception = await Assert.ThrowsAsync(() => app(new DefaultHttpContext())); + Assert.Equal( + Resources.FormatException_InvokeMiddlewareNoService( + typeof(object), + typeof(MiddlewareInjectInvokeNoService)), + exception.Message); + } + + [Fact] + public void UseMiddlewareWithInvokeArg() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareInjectInvoke)); + var app = builder.Build(); + app(new DefaultHttpContext()); + } + + [Fact] + public void UseMiddlewareWithIvokeWithOutAndRefThrows() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(MiddlewareInjectWithOutAndRefParams)); + var exception = Assert.Throws(() => builder.Build()); + } + + [Fact] + public void UseMiddlewareWithIMiddlewareThrowsIfParametersSpecified() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + var exception = Assert.Throws(() => builder.UseMiddleware(typeof(Middleware), "arg")); + Assert.Equal(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)), exception.Message); + } + + [Fact] + public async Task UseMiddlewareWithIMiddlewareThrowsIfNoIMiddlewareFactoryRegistered() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(Middleware)); + var app = builder.Build(); + var exception = await Assert.ThrowsAsync(async () => + { + var context = new DefaultHttpContext(); + var sp = new DummyServiceProvider(); + context.RequestServices = sp; + await app(context); + }); + Assert.Equal(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)), exception.Message); + } + + [Fact] + public async Task UseMiddlewareWithIMiddlewareThrowsIfMiddlewareFactoryCreateReturnsNull() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(Middleware)); + var app = builder.Build(); + var exception = await Assert.ThrowsAsync(async () => + { + var context = new DefaultHttpContext(); + var sp = new DummyServiceProvider(); + sp.AddService(typeof(IMiddlewareFactory), new BadMiddlewareFactory()); + context.RequestServices = sp; + await app(context); + }); + + Assert.Equal( + Resources.FormatException_UseMiddlewareUnableToCreateMiddleware( + typeof(BadMiddlewareFactory), + typeof(Middleware)), + exception.Message); + } + + [Fact] + public async Task UseMiddlewareWithIMiddlewareWorks() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(Middleware)); + var app = builder.Build(); + var context = new DefaultHttpContext(); + var sp = new DummyServiceProvider(); + var middlewareFactory = new BasicMiddlewareFactory(); + sp.AddService(typeof(IMiddlewareFactory), middlewareFactory); + context.RequestServices = sp; + await app(context); + Assert.True(Assert.IsType(context.Items["before"])); + Assert.True(Assert.IsType(context.Items["after"])); + Assert.NotNull(middlewareFactory.Created); + Assert.NotNull(middlewareFactory.Released); + Assert.IsType(middlewareFactory.Created); + Assert.IsType(middlewareFactory.Released); + Assert.Same(middlewareFactory.Created, middlewareFactory.Released); + } + + public class Middleware : IMiddleware + { + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + context.Items["before"] = true; + await next(context); + context.Items["after"] = true; + } + } + + public class BasicMiddlewareFactory : IMiddlewareFactory + { + public IMiddleware Created { get; private set; } + public IMiddleware Released { get; private set; } + + public IMiddleware Create(Type middlewareType) + { + Created = Activator.CreateInstance(middlewareType) as IMiddleware; + return Created; + } + + public void Release(IMiddleware middleware) + { + Released = middleware; + } + } + + public class BadMiddlewareFactory : IMiddlewareFactory + { + public IMiddleware Create(Type middlewareType) => null; + + public void Release(IMiddleware middleware) { } + } + + private class DummyServiceProvider : IServiceProvider + { + private Dictionary _services = new Dictionary(); + + public void AddService(Type type, object value) => _services[type] = value; + + public object GetService(Type serviceType) + { + if (serviceType == typeof(IServiceProvider)) + { + return this; + } + + if (_services.TryGetValue(serviceType, out object value)) + { + return value; + } + return null; + } + } + + public class MiddlewareInjectWithOutAndRefParams + { + public MiddlewareInjectWithOutAndRefParams(RequestDelegate next) { } + + public Task Invoke(HttpContext context, ref IServiceProvider sp1, out IServiceProvider sp2) + { + sp1 = null; + sp2 = null; + return Task.FromResult(0); + } + } + + private class MiddlewareInjectInvokeNoService + { + public MiddlewareInjectInvokeNoService(RequestDelegate next) { } + + public Task Invoke(HttpContext context, object value) => Task.CompletedTask; + } + + private class MiddlewareInjectInvoke + { + public MiddlewareInjectInvoke(RequestDelegate next) { } + + public Task Invoke(HttpContext context, IServiceProvider provider) => Task.CompletedTask; + } + + private class MiddlewareNoParametersStub + { + public MiddlewareNoParametersStub(RequestDelegate next) { } + + public Task Invoke() => Task.CompletedTask; + } + + private class MiddlewareAsyncNoParametersStub + { + public MiddlewareAsyncNoParametersStub(RequestDelegate next) { } + + public Task InvokeAsync() => Task.CompletedTask; + } + + private class MiddlewareNonTaskReturnStub + { + public MiddlewareNonTaskReturnStub(RequestDelegate next) { } + + public int Invoke() => 0; + } + + private class MiddlewareAsyncNonTaskReturnStub + { + public MiddlewareAsyncNonTaskReturnStub(RequestDelegate next) { } + + public int InvokeAsync() => 0; + } + + private class MiddlewareNoInvokeStub + { + public MiddlewareNoInvokeStub(RequestDelegate next) { } + } + + private class MiddlewareMultipleInvokesStub + { + public MiddlewareMultipleInvokesStub(RequestDelegate next) { } + + public Task Invoke(HttpContext context) => Task.CompletedTask; + + public Task Invoke(HttpContext context, int i) => Task.CompletedTask; + } + + private class MiddlewareMultipleInvokeAsyncStub + { + public MiddlewareMultipleInvokeAsyncStub(RequestDelegate next) { } + + public Task InvokeAsync(HttpContext context) => Task.CompletedTask; + + public Task InvokeAsync(HttpContext context, int i) => Task.CompletedTask; + } + + private class MiddlewareMultipleInvokeAndInvokeAsyncStub + { + public MiddlewareMultipleInvokeAndInvokeAsyncStub(RequestDelegate next) { } + + public Task Invoke(HttpContext context) => Task.CompletedTask; + + public Task InvokeAsync(HttpContext context) => Task.CompletedTask; + } + } +} diff --git a/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs b/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs new file mode 100644 index 0000000000..4b8e9d71cb --- /dev/null +++ b/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs @@ -0,0 +1,168 @@ +// 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.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + public class UsePathBaseExtensionsTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("/")] + public void EmptyOrNullPathBase_DoNotAddMiddleware(string pathBase) + { + // Arrange + var useCalled = false; + var builder = new ApplicationBuilderWrapper(CreateBuilder(), () => useCalled = true) + .UsePathBase(pathBase); + + // Act + builder.Build(); + + // Assert + Assert.False(useCalled); + } + + private class ApplicationBuilderWrapper : IApplicationBuilder + { + private readonly IApplicationBuilder _wrappedBuilder; + private readonly Action _useCallback; + + public ApplicationBuilderWrapper(IApplicationBuilder applicationBuilder, Action useCallback) + { + _wrappedBuilder = applicationBuilder; + _useCallback = useCallback; + } + + public IApplicationBuilder Use(Func middleware) + { + _useCallback(); + return _wrappedBuilder.Use(middleware); + } + + public IServiceProvider ApplicationServices + { + get { return _wrappedBuilder.ApplicationServices; } + set { _wrappedBuilder.ApplicationServices = value; } + } + + public IDictionary Properties => _wrappedBuilder.Properties; + public IFeatureCollection ServerFeatures => _wrappedBuilder.ServerFeatures; + public RequestDelegate Build() => _wrappedBuilder.Build(); + public IApplicationBuilder New() => _wrappedBuilder.New(); + + } + + [Theory] + [InlineData("/base", "", "/base", "/base", "")] + [InlineData("/base", "", "/base/", "/base", "/")] + [InlineData("/base", "", "/base/something", "/base", "/something")] + [InlineData("/base", "", "/base/something/", "/base", "/something/")] + [InlineData("/base/more", "", "/base/more", "/base/more", "")] + [InlineData("/base/more", "", "/base/more/something", "/base/more", "/something")] + [InlineData("/base/more", "", "/base/more/something/", "/base/more", "/something/")] + [InlineData("/base", "/oldbase", "/base", "/oldbase/base", "")] + [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] + [InlineData("/base", "/oldbase", "/base/something", "/oldbase/base", "/something")] + [InlineData("/base", "/oldbase", "/base/something/", "/oldbase/base", "/something/")] + [InlineData("/base/more", "/oldbase", "/base/more", "/oldbase/base/more", "")] + [InlineData("/base/more", "/oldbase", "/base/more/something", "/oldbase/base/more", "/something")] + [InlineData("/base/more", "/oldbase", "/base/more/something/", "/oldbase/base/more", "/something/")] + public void RequestPathBaseContainingPathBase_IsSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/base", "", "/something", "", "/something")] + [InlineData("/base", "", "/baseandsomething", "", "/baseandsomething")] + [InlineData("/base", "", "/ba", "", "/ba")] + [InlineData("/base", "", "/ba/se", "", "/ba/se")] + [InlineData("/base", "/oldbase", "/something", "/oldbase", "/something")] + [InlineData("/base", "/oldbase", "/baseandsomething", "/oldbase", "/baseandsomething")] + [InlineData("/base", "/oldbase", "/ba", "/oldbase", "/ba")] + [InlineData("/base", "/oldbase", "/ba/se", "/oldbase", "/ba/se")] + public void RequestPathBaseNotContainingPathBase_IsNotSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("", "", "/", "", "/")] + [InlineData("/", "", "/", "", "/")] + [InlineData("/base", "", "/base/", "/base", "/")] + [InlineData("/base/", "", "/base", "/base", "")] + [InlineData("/base/", "", "/base/", "/base", "/")] + [InlineData("", "/oldbase", "/", "/oldbase", "/")] + [InlineData("/", "/oldbase", "/", "/oldbase", "/")] + [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] + [InlineData("/base/", "/oldbase", "/base", "/oldbase/base", "")] + [InlineData("/base/", "/oldbase", "/base/", "/oldbase/base", "/")] + public void PathBaseNeverEndsWithSlash(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/base", "", "/Base/Something", "/Base", "/Something")] + [InlineData("/base", "/OldBase", "/Base/Something", "/OldBase/Base", "/Something")] + public void PathBaseAndPathPreserveRequestCasing(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/b♫se", "", "/b♫se/something", "/b♫se", "/something")] + [InlineData("/b♫se", "", "/B♫se/something", "/B♫se", "/something")] + [InlineData("/b♫se", "", "/b♫se/Something", "/b♫se", "/Something")] + [InlineData("/b♫se", "/oldb♫se", "/b♫se/something", "/oldb♫se/b♫se", "/something")] + [InlineData("/b♫se", "/oldb♫se", "/b♫se/Something", "/oldb♫se/b♫se", "/Something")] + [InlineData("/b♫se", "/oldb♫se", "/B♫se/something", "/oldb♫se/B♫se", "/something")] + public void PathBaseCanHaveUnicodeCharacters(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + private static void TestPathBase(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + HttpContext requestContext = CreateRequest(pathBase, requestPath); + var builder = CreateBuilder() + .UsePathBase(registeredPathBase); + builder.Run(context => + { + context.Items["test.Path"] = context.Request.Path; + context.Items["test.PathBase"] = context.Request.PathBase; + return Task.FromResult(0); + }); + builder.Build().Invoke(requestContext).Wait(); + + // Assert path and pathBase are split after middleware + Assert.Equal(expectedPath, ((PathString)requestContext.Items["test.Path"]).Value); + Assert.Equal(expectedPathBase, ((PathString)requestContext.Items["test.PathBase"]).Value); + // Assert path and pathBase are reset after request + Assert.Equal(pathBase, requestContext.Request.PathBase.Value); + Assert.Equal(requestPath, requestContext.Request.Path.Value); + } + + private static HttpContext CreateRequest(string pathBase, string requestPath) + { + HttpContext context = new DefaultHttpContext(); + context.Request.PathBase = new PathString(pathBase); + context.Request.Path = new PathString(requestPath); + return context; + } + + private static ApplicationBuilder CreateBuilder() + { + return new ApplicationBuilder(serviceProvider: null); + } + } +} diff --git a/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs b/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs new file mode 100644 index 0000000000..902a003b26 --- /dev/null +++ b/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs @@ -0,0 +1,170 @@ +// 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.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + public class UseWhenExtensionsTests + { + [Fact] + public void NullArguments_ArgumentNullException() + { + // Arrange + var builder = CreateBuilder(); + + // Act + Action nullPredicate = () => builder.UseWhen(null, app => { }); + Action nullConfiguration = () => builder.UseWhen(TruePredicate, null); + + // Assert + Assert.Throws(nullPredicate); + Assert.Throws(nullConfiguration); + } + + [Fact] + public void PredicateTrue_BranchTaken_WillRejoin() + { + // Arrange + var context = CreateContext(); + var parent = CreateBuilder(); + + parent.UseWhen(TruePredicate, child => + { + child.UseWhen(TruePredicate, grandchild => + { + grandchild.Use(Increment("grandchild")); + }); + + child.Use(Increment("child")); + }); + + parent.Use(Increment("parent")); + + // Act + parent.Build().Invoke(context).Wait(); + + // Assert + Assert.Equal(1, Count(context, "parent")); + Assert.Equal(1, Count(context, "child")); + Assert.Equal(1, Count(context, "grandchild")); + } + + [Fact] + public void PredicateTrue_BranchTaken_CanTerminate() + { + // Arrange + var context = CreateContext(); + var parent = CreateBuilder(); + + parent.UseWhen(TruePredicate, child => + { + child.UseWhen(TruePredicate, grandchild => + { + grandchild.Use(Increment("grandchild", terminate: true)); + }); + + child.Use(Increment("child")); + }); + + parent.Use(Increment("parent")); + + // Act + parent.Build().Invoke(context).Wait(); + + // Assert + Assert.Equal(0, Count(context, "parent")); + Assert.Equal(0, Count(context, "child")); + Assert.Equal(1, Count(context, "grandchild")); + } + + [Fact] + public void PredicateFalse_PassThrough() + { + // Arrange + var context = CreateContext(); + var parent = CreateBuilder(); + + parent.UseWhen(FalsePredicate, child => + { + child.Use(Increment("child")); + }); + + parent.Use(Increment("parent")); + + // Act + parent.Build().Invoke(context).Wait(); + + // Assert + Assert.Equal(1, Count(context, "parent")); + Assert.Equal(0, Count(context, "child")); + } + + private static HttpContext CreateContext() + { + return new DefaultHttpContext(); + } + + private static ApplicationBuilder CreateBuilder() + { + return new ApplicationBuilder(serviceProvider: null); + } + + private static bool TruePredicate(HttpContext context) + { + return true; + } + + private static bool FalsePredicate(HttpContext context) + { + return false; + } + + private static Func, Task> Increment(string key, bool terminate = false) + { + return (context, next) => + { + if (!context.Items.ContainsKey(key)) + { + context.Items[key] = 1; + } + else + { + var item = context.Items[key]; + + if (item is int) + { + context.Items[key] = 1 + (int)item; + } + else + { + context.Items[key] = 1; + } + } + + return terminate ? Task.FromResult(null) : next(); + }; + } + + private static int Count(HttpContext context, string key) + { + if (!context.Items.ContainsKey(key)) + { + return 0; + } + + var item = context.Items[key]; + + if (item is int) + { + return (int)item; + } + + return 0; + } + } +} diff --git a/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs b/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs new file mode 100644 index 0000000000..1723ee6fd5 --- /dev/null +++ b/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs @@ -0,0 +1,287 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Http.Headers; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http +{ + public static class HeaderDictionaryTypeExtensions + { + public static RequestHeaders GetTypedHeaders(this HttpRequest request) + { + return new RequestHeaders(request.Headers); + } + + public static ResponseHeaders GetTypedHeaders(this HttpResponse response) + { + return new ResponseHeaders(response.Headers); + } + + // These are all shared helpers used by both RequestHeaders and ResponseHeaders + + internal static DateTimeOffset? GetDate(this IHeaderDictionary headers, string name) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return headers.Get(name); + } + + internal static void Set(this IHeaderDictionary headers, string name, object value) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + headers.Remove(name); + } + else + { + headers[name] = value.ToString(); + } + } + + internal static void SetList(this IHeaderDictionary headers, string name, IList values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (values == null || values.Count == 0) + { + headers.Remove(name); + } + else if (values.Count == 1) + { + headers[name] = new StringValues(values[0].ToString()); + } + else + { + var newValues = new string[values.Count]; + for (var i = 0; i < values.Count; i++) + { + newValues[i] = values[i].ToString(); + } + headers[name] = new StringValues(newValues); + } + } + + public static void AppendList(this IHeaderDictionary Headers, string name, IList values) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + switch (values.Count) + { + case 0: + Headers.Append(name, StringValues.Empty); + break; + case 1: + Headers.Append(name, new StringValues(values[0].ToString())); + break; + default: + var newValues = new string[values.Count]; + for (var i = 0; i < values.Count; i++) + { + newValues[i] = values[i].ToString(); + } + Headers.Append(name, new StringValues(newValues)); + break; + } + } + + internal static void SetDate(this IHeaderDictionary headers, string name, DateTimeOffset? value) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value.HasValue) + { + headers[name] = HeaderUtilities.FormatDate(value.Value); + } + else + { + headers.Remove(name); + } + } + + private static IDictionary KnownParsers = new Dictionary() + { + { typeof(CacheControlHeaderValue), new Func(value => { CacheControlHeaderValue result; return CacheControlHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(ContentDispositionHeaderValue), new Func(value => { ContentDispositionHeaderValue result; return ContentDispositionHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(ContentRangeHeaderValue), new Func(value => { ContentRangeHeaderValue result; return ContentRangeHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(MediaTypeHeaderValue), new Func(value => { MediaTypeHeaderValue result; return MediaTypeHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(RangeConditionHeaderValue), new Func(value => { RangeConditionHeaderValue result; return RangeConditionHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(RangeHeaderValue), new Func(value => { RangeHeaderValue result; return RangeHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(EntityTagHeaderValue), new Func(value => { EntityTagHeaderValue result; return EntityTagHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(DateTimeOffset?), new Func(value => { DateTimeOffset result; return HeaderUtilities.TryParseDate(value, out result) ? result : (DateTimeOffset?)null; }) }, + { typeof(long?), new Func(value => { long result; return HeaderUtilities.TryParseNonNegativeInt64(value, out result) ? result : (long?)null; }) }, + }; + + private static IDictionary KnownListParsers = new Dictionary() + { + { typeof(MediaTypeHeaderValue), new Func, IList>(value => { IList result; return MediaTypeHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(StringWithQualityHeaderValue), new Func, IList>(value => { IList result; return StringWithQualityHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(CookieHeaderValue), new Func, IList>(value => { IList result; return CookieHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(EntityTagHeaderValue), new Func, IList>(value => { IList result; return EntityTagHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(SetCookieHeaderValue), new Func, IList>(value => { IList result; return SetCookieHeaderValue.TryParseList(value, out result) ? result : null; }) }, + }; + + internal static T Get(this IHeaderDictionary headers, string name) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + object temp; + var value = headers[name]; + + if (StringValues.IsNullOrEmpty(value)) + { + return default(T); + } + + if (KnownParsers.TryGetValue(typeof(T), out temp)) + { + var func = (Func)temp; + return func(value); + } + + return GetViaReflection(value.ToString()); + } + + internal static IList GetList(this IHeaderDictionary headers, string name) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + object temp; + var values = headers[name]; + + if (StringValues.IsNullOrEmpty(values)) + { + return null; + } + + if (KnownListParsers.TryGetValue(typeof(T), out temp)) + { + var func = (Func, IList>)temp; + return func(values); + } + + return GetListViaReflection(values); + } + + private static T GetViaReflection(string value) + { + // TODO: Cache the reflected type for later? Only if success? + var type = typeof(T); + var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(methodInfo => + { + if (string.Equals("TryParse", methodInfo.Name, StringComparison.Ordinal) + && methodInfo.ReturnParameter.ParameterType.Equals(typeof(bool))) + { + var methodParams = methodInfo.GetParameters(); + return methodParams.Length == 2 + && methodParams[0].ParameterType.Equals(typeof(string)) + && methodParams[1].IsOut + && methodParams[1].ParameterType.Equals(type.MakeByRefType()); + } + return false; + }); + + if (method == null) + { + throw new NotSupportedException(string.Format( + "The given type '{0}' does not have a TryParse method with the required signature 'public static bool TryParse(string, out {0}).", nameof(T))); + } + + var parameters = new object[] { value, null }; + var success = (bool)method.Invoke(null, parameters); + if (success) + { + return (T)parameters[1]; + } + return default(T); + } + + private static IList GetListViaReflection(StringValues values) + { + // TODO: Cache the reflected type for later? Only if success? + var type = typeof(T); + var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(methodInfo => + { + if (string.Equals("TryParseList", methodInfo.Name, StringComparison.Ordinal) + && methodInfo.ReturnParameter.ParameterType.Equals(typeof(Boolean))) + { + var methodParams = methodInfo.GetParameters(); + return methodParams.Length == 2 + && methodParams[0].ParameterType.Equals(typeof(IList)) + && methodParams[1].IsOut + && methodParams[1].ParameterType.Equals(typeof(IList).MakeByRefType()); + } + return false; + }); + + if (method == null) + { + throw new NotSupportedException(string.Format( + "The given type '{0}' does not have a TryParseList method with the required signature 'public static bool TryParseList(IList, out IList<{0}>).", nameof(T))); + } + + var parameters = new object[] { values, null }; + var success = (bool)method.Invoke(null, parameters); + if (success) + { + return (IList)parameters[1]; + } + return null; + } + } +} diff --git a/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs new file mode 100644 index 0000000000..da9188dad3 --- /dev/null +++ b/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs @@ -0,0 +1,26 @@ +// 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.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + public static class HttpRequestMultipartExtensions + { + public static string GetMultipartBoundary(this HttpRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + MediaTypeHeaderValue mediaType; + if (!MediaTypeHeaderValue.TryParse(request.ContentType, out mediaType)) + { + return string.Empty; + } + return HeaderUtilities.RemoveQuotes(mediaType.Boundary).ToString(); + } + } +} diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj new file mode 100644 index 0000000000..25ae2af17a --- /dev/null +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -0,0 +1,18 @@ + + + + ASP.NET Core common extension methods for HTTP abstractions, HTTP headers, HTTP request/response, and session state. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + + diff --git a/src/Http/Http.Extensions/src/QueryBuilder.cs b/src/Http/Http.Extensions/src/QueryBuilder.cs new file mode 100644 index 0000000000..e9feb391b1 --- /dev/null +++ b/src/Http/Http.Extensions/src/QueryBuilder.cs @@ -0,0 +1,81 @@ +// 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; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + // The IEnumerable interface is required for the collection initialization syntax: new QueryBuilder() { { "key", "value" } }; + public class QueryBuilder : IEnumerable> + { + private IList> _params; + + public QueryBuilder() + { + _params = new List>(); + } + + public QueryBuilder(IEnumerable> parameters) + { + _params = new List>(parameters); + } + + public void Add(string key, IEnumerable values) + { + foreach (var value in values) + { + _params.Add(new KeyValuePair(key, value)); + } + } + + public void Add(string key, string value) + { + _params.Add(new KeyValuePair(key, value)); + } + + public override string ToString() + { + var builder = new StringBuilder(); + bool first = true; + for (int i = 0; i < _params.Count; i++) + { + var pair = _params[i]; + builder.Append(first ? "?" : "&"); + first = false; + builder.Append(UrlEncoder.Default.Encode(pair.Key)); + builder.Append("="); + builder.Append(UrlEncoder.Default.Encode(pair.Value)); + } + + return builder.ToString(); + } + + public QueryString ToQueryString() + { + return new QueryString(ToString()); + } + + public override int GetHashCode() + { + return ToQueryString().GetHashCode(); + } + + public override bool Equals(object obj) + { + return ToQueryString().Equals(obj); + } + + public IEnumerator> GetEnumerator() + { + return _params.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _params.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/RequestHeaders.cs b/src/Http/Http.Extensions/src/RequestHeaders.cs new file mode 100644 index 0000000000..12246922d4 --- /dev/null +++ b/src/Http/Http.Extensions/src/RequestHeaders.cs @@ -0,0 +1,332 @@ +// 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.Extensions; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Headers +{ + public class RequestHeaders + { + public RequestHeaders(IHeaderDictionary headers) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + Headers = headers; + } + + public IHeaderDictionary Headers { get; } + + public IList Accept + { + get + { + return Headers.GetList(HeaderNames.Accept); + } + set + { + Headers.SetList(HeaderNames.Accept, value); + } + } + + public IList AcceptCharset + { + get + { + return Headers.GetList(HeaderNames.AcceptCharset); + } + set + { + Headers.SetList(HeaderNames.AcceptCharset, value); + } + } + + public IList AcceptEncoding + { + get + { + return Headers.GetList(HeaderNames.AcceptEncoding); + } + set + { + Headers.SetList(HeaderNames.AcceptEncoding, value); + } + } + + public IList AcceptLanguage + { + get + { + return Headers.GetList(HeaderNames.AcceptLanguage); + } + set + { + Headers.SetList(HeaderNames.AcceptLanguage, value); + } + } + + public CacheControlHeaderValue CacheControl + { + get + { + return Headers.Get(HeaderNames.CacheControl); + } + set + { + Headers.Set(HeaderNames.CacheControl, value); + } + } + + public ContentDispositionHeaderValue ContentDisposition + { + get + { + return Headers.Get(HeaderNames.ContentDisposition); + } + set + { + Headers.Set(HeaderNames.ContentDisposition, value); + } + } + + public long? ContentLength + { + get + { + return Headers.ContentLength; + } + set + { + Headers.ContentLength = value; + } + } + + public ContentRangeHeaderValue ContentRange + { + get + { + return Headers.Get(HeaderNames.ContentRange); + } + set + { + Headers.Set(HeaderNames.ContentRange, value); + } + } + + public MediaTypeHeaderValue ContentType + { + get + { + return Headers.Get(HeaderNames.ContentType); + } + set + { + Headers.Set(HeaderNames.ContentType, value); + } + } + + public IList Cookie + { + get + { + return Headers.GetList(HeaderNames.Cookie); + } + set + { + Headers.SetList(HeaderNames.Cookie, value); + } + } + + public DateTimeOffset? Date + { + get + { + return Headers.GetDate(HeaderNames.Date); + } + set + { + Headers.SetDate(HeaderNames.Date, value); + } + } + + public DateTimeOffset? Expires + { + get + { + return Headers.GetDate(HeaderNames.Expires); + } + set + { + Headers.SetDate(HeaderNames.Expires, value); + } + } + + public HostString Host + { + get + { + return HostString.FromUriComponent(Headers[HeaderNames.Host]); + } + set + { + Headers[HeaderNames.Host] = value.ToUriComponent(); + } + } + + public IList IfMatch + { + get + { + return Headers.GetList(HeaderNames.IfMatch); + } + set + { + Headers.SetList(HeaderNames.IfMatch, value); + } + } + + public DateTimeOffset? IfModifiedSince + { + get + { + return Headers.GetDate(HeaderNames.IfModifiedSince); + } + set + { + Headers.SetDate(HeaderNames.IfModifiedSince, value); + } + } + + public IList IfNoneMatch + { + get + { + return Headers.GetList(HeaderNames.IfNoneMatch); + } + set + { + Headers.SetList(HeaderNames.IfNoneMatch, value); + } + } + + public RangeConditionHeaderValue IfRange + { + get + { + return Headers.Get(HeaderNames.IfRange); + } + set + { + Headers.Set(HeaderNames.IfRange, value); + } + } + + public DateTimeOffset? IfUnmodifiedSince + { + get + { + return Headers.GetDate(HeaderNames.IfUnmodifiedSince); + } + set + { + Headers.SetDate(HeaderNames.IfUnmodifiedSince, value); + } + } + + public DateTimeOffset? LastModified + { + get + { + return Headers.GetDate(HeaderNames.LastModified); + } + set + { + Headers.SetDate(HeaderNames.LastModified, value); + } + } + + public RangeHeaderValue Range + { + get + { + return Headers.Get(HeaderNames.Range); + } + set + { + Headers.Set(HeaderNames.Range, value); + } + } + + public Uri Referer + { + get + { + Uri uri; + if (Uri.TryCreate(Headers[HeaderNames.Referer], UriKind.RelativeOrAbsolute, out uri)) + { + return uri; + } + return null; + } + set + { + Headers.Set(HeaderNames.Referer, value == null ? null : UriHelper.Encode(value)); + } + } + + public T Get(string name) + { + return Headers.Get(name); + } + + public IList GetList(string name) + { + return Headers.GetList(name); + } + + public void Set(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Headers.Set(name, value); + } + + public void SetList(string name, IList values) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Headers.SetList(name, values); + } + + public void Append(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Headers.Append(name, value.ToString()); + } + + public void AppendList(string name, IList values) + { + Headers.AppendList(name, values); + } + } +} diff --git a/src/Http/Http.Extensions/src/ResponseExtensions.cs b/src/Http/Http.Extensions/src/ResponseExtensions.cs new file mode 100644 index 0000000000..6c5d92a7af --- /dev/null +++ b/src/Http/Http.Extensions/src/ResponseExtensions.cs @@ -0,0 +1,26 @@ +// 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.Features; + +namespace Microsoft.AspNetCore.Http +{ + public static class ResponseExtensions + { + public static void Clear(this HttpResponse response) + { + if (response.HasStarted) + { + throw new InvalidOperationException("The response cannot be cleared, it has already started sending."); + } + response.StatusCode = 200; + response.HttpContext.Features.Get().ReasonPhrase = null; + response.Headers.Clear(); + if (response.Body.CanSeek) + { + response.Body.SetLength(0); + } + } + } +} diff --git a/src/Http/Http.Extensions/src/ResponseHeaders.cs b/src/Http/Http.Extensions/src/ResponseHeaders.cs new file mode 100644 index 0000000000..87e3c0318c --- /dev/null +++ b/src/Http/Http.Extensions/src/ResponseHeaders.cs @@ -0,0 +1,211 @@ +// 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.Extensions; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Headers +{ + public class ResponseHeaders + { + public ResponseHeaders(IHeaderDictionary headers) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + Headers = headers; + } + + public IHeaderDictionary Headers { get; } + + public CacheControlHeaderValue CacheControl + { + get + { + return Headers.Get(HeaderNames.CacheControl); + } + set + { + Headers.Set(HeaderNames.CacheControl, value); + } + } + + public ContentDispositionHeaderValue ContentDisposition + { + get + { + return Headers.Get(HeaderNames.ContentDisposition); + } + set + { + Headers.Set(HeaderNames.ContentDisposition, value); + } + } + + public long? ContentLength + { + get + { + return Headers.ContentLength; + } + set + { + Headers.ContentLength = value; + } + } + + public ContentRangeHeaderValue ContentRange + { + get + { + return Headers.Get(HeaderNames.ContentRange); + } + set + { + Headers.Set(HeaderNames.ContentRange, value); + } + } + + public MediaTypeHeaderValue ContentType + { + get + { + return Headers.Get(HeaderNames.ContentType); + } + set + { + Headers.Set(HeaderNames.ContentType, value); + } + } + + public DateTimeOffset? Date + { + get + { + return Headers.GetDate(HeaderNames.Date); + } + set + { + Headers.SetDate(HeaderNames.Date, value); + } + } + + public EntityTagHeaderValue ETag + { + get + { + return Headers.Get(HeaderNames.ETag); + } + set + { + Headers.Set(HeaderNames.ETag, value); + } + } + public DateTimeOffset? Expires + { + get + { + return Headers.GetDate(HeaderNames.Expires); + } + set + { + Headers.SetDate(HeaderNames.Expires, value); + } + } + + public DateTimeOffset? LastModified + { + get + { + return Headers.GetDate(HeaderNames.LastModified); + } + set + { + Headers.SetDate(HeaderNames.LastModified, value); + } + } + + public Uri Location + { + get + { + Uri uri; + if (Uri.TryCreate(Headers[HeaderNames.Location], UriKind.RelativeOrAbsolute, out uri)) + { + return uri; + } + return null; + } + set + { + Headers.Set(HeaderNames.Location, value == null ? null : UriHelper.Encode(value)); + } + } + + public IList SetCookie + { + get + { + return Headers.GetList(HeaderNames.SetCookie); + } + set + { + Headers.SetList(HeaderNames.SetCookie, value); + } + } + + public T Get(string name) + { + return Headers.Get(name); + } + + public IList GetList(string name) + { + return Headers.GetList(name); + } + + public void Set(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Headers.Set(name, value); + } + + public void SetList(string name, IList values) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Headers.SetList(name, values); + } + + public void Append(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Headers.Append(name, value.ToString()); + } + + public void AppendList(string name, IList values) + { + Headers.AppendList(name, values); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs b/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs new file mode 100644 index 0000000000..74c0422ef4 --- /dev/null +++ b/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs @@ -0,0 +1,181 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Provides extensions for HttpResponse exposing the SendFile extension. + /// + public static class SendFileResponseExtensions + { + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// The file. + /// The . + public static Task SendFileAsync(this HttpResponse response, IFileInfo file, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + return SendFileAsyncCore(response, file, 0, null, cancellationToken); + } + + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// The file. + /// The offset in the file. + /// The number of bytes to send, or null to send the remainder of the file. + /// + /// + public static Task SendFileAsync(this HttpResponse response, IFileInfo file, long offset, long? count, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + return SendFileAsyncCore(response, file, offset, count, cancellationToken); + } + + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// The full path to the file. + /// The . + /// + public static Task SendFileAsync(this HttpResponse response, string fileName, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + return SendFileAsyncCore(response, fileName, 0, null, cancellationToken); + } + + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// The full path to the file. + /// The offset in the file. + /// The number of bytes to send, or null to send the remainder of the file. + /// + /// + public static Task SendFileAsync(this HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + return SendFileAsyncCore(response, fileName, offset, count, cancellationToken); + } + + private static async Task SendFileAsyncCore(HttpResponse response, IFileInfo file, long offset, long? count, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(file.PhysicalPath)) + { + CheckRange(offset, count, file.Length); + + using (var fileContent = file.CreateReadStream()) + { + if (offset > 0) + { + fileContent.Seek(offset, SeekOrigin.Begin); + } + await StreamCopyOperation.CopyToAsync(fileContent, response.Body, count, cancellationToken); + } + } + else + { + await response.SendFileAsync(file.PhysicalPath, offset, count, cancellationToken); + } + } + + private static Task SendFileAsyncCore(HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default) + { + var sendFile = response.HttpContext.Features.Get(); + if (sendFile == null) + { + return SendFileAsyncCore(response.Body, fileName, offset, count, cancellationToken); + } + + return sendFile.SendFileAsync(fileName, offset, count, cancellationToken); + } + + // Not safe for overlapped writes. + private static async Task SendFileAsyncCore(Stream outputStream, string fileName, long offset, long? count, CancellationToken cancel = default) + { + cancel.ThrowIfCancellationRequested(); + + var fileInfo = new FileInfo(fileName); + CheckRange(offset, count, fileInfo.Length); + + int bufferSize = 1024 * 16; + var fileStream = new FileStream( + fileName, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize: bufferSize, + options: FileOptions.Asynchronous | FileOptions.SequentialScan); + + using (fileStream) + { + if (offset > 0) + { + fileStream.Seek(offset, SeekOrigin.Begin); + } + + await StreamCopyOperation.CopyToAsync(fileStream, outputStream, count, cancel); + } + } + + private static void CheckRange(long offset, long? count, long fileLength) + { + if (offset < 0 || offset > fileLength) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); + } + if (count.HasValue && + (count.Value < 0 || count.Value > fileLength - offset)) + { + throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/SessionExtensions.cs b/src/Http/Http.Extensions/src/SessionExtensions.cs new file mode 100644 index 0000000000..fd7573fa95 --- /dev/null +++ b/src/Http/Http.Extensions/src/SessionExtensions.cs @@ -0,0 +1,54 @@ +// 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.Text; + +namespace Microsoft.AspNetCore.Http +{ + public static class SessionExtensions + { + public static void SetInt32(this ISession session, string key, int value) + { + var bytes = new byte[] + { + (byte)(value >> 24), + (byte)(0xFF & (value >> 16)), + (byte)(0xFF & (value >> 8)), + (byte)(0xFF & value) + }; + session.Set(key, bytes); + } + + public static int? GetInt32(this ISession session, string key) + { + var data = session.Get(key); + if (data == null || data.Length < 4) + { + return null; + } + return data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + } + + public static void SetString(this ISession session, string key, string value) + { + session.Set(key, Encoding.UTF8.GetBytes(value)); + } + + public static string GetString(this ISession session, string key) + { + var data = session.Get(key); + if (data == null) + { + return null; + } + return Encoding.UTF8.GetString(data); + } + + public static byte[] Get(this ISession session, string key) + { + byte[] value = null; + session.TryGetValue(key, out value); + return value; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/StreamCopyOperation.cs b/src/Http/Http.Extensions/src/StreamCopyOperation.cs new file mode 100644 index 0000000000..12067fef65 --- /dev/null +++ b/src/Http/Http.Extensions/src/StreamCopyOperation.cs @@ -0,0 +1,87 @@ +// 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.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + // FYI: In most cases the source will be a FileStream and the destination will be to the network. + public static class StreamCopyOperation + { + private const int DefaultBufferSize = 4096; + + /// Asynchronously reads the bytes from the source stream and writes them to another stream. + /// A task that represents the asynchronous copy operation. + /// The stream from which the contents will be copied. + /// The stream to which the contents of the current stream will be copied. + /// The count of bytes to be copied. + /// The token to monitor for cancellation requests. The default value is . + public static Task CopyToAsync(Stream source, Stream destination, long? count, CancellationToken cancel) + { + return CopyToAsync(source, destination, count, DefaultBufferSize, cancel); + } + + /// Asynchronously reads the bytes from the source stream and writes them to another stream, using a specified buffer size. + /// A task that represents the asynchronous copy operation. + /// The stream from which the contents will be copied. + /// The stream to which the contents of the current stream will be copied. + /// The count of bytes to be copied. + /// The size, in bytes, of the buffer. This value must be greater than zero. The default size is 4096. + /// The token to monitor for cancellation requests. The default value is . + public static async Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel) + { + long? bytesRemaining = count; + + var buffer = ArrayPool.Shared.Rent(bufferSize); + try + { + Debug.Assert(source != null); + Debug.Assert(destination != null); + Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.Value >= 0); + Debug.Assert(buffer != null); + + while (true) + { + // The natural end of the range. + if (bytesRemaining.HasValue && bytesRemaining.Value <= 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + int readLength = buffer.Length; + if (bytesRemaining.HasValue) + { + readLength = (int)Math.Min(bytesRemaining.Value, (long)readLength); + } + int read = await source.ReadAsync(buffer, 0, readLength, cancel); + + if (bytesRemaining.HasValue) + { + bytesRemaining -= read; + } + + // End of the source stream. + if (read == 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + await destination.WriteAsync(buffer, 0, read, cancel); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs new file mode 100644 index 0000000000..633e591186 --- /dev/null +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -0,0 +1,217 @@ +// 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; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + /// + /// A helper class for constructing encoded Uris for use in headers and other Uris. + /// + public static class UriHelper + { + private const string ForwardSlash = "/"; + private const string Pound = "#"; + private const string QuestionMark = "?"; + private const string SchemeDelimiter = "://"; + + /// + /// Combines the given URI components into a string that is properly encoded for use in HTTP headers. + /// + /// The first portion of the request path associated with application root. + /// The portion of the request path that identifies the requested resource. + /// The query, if any. + /// The fragment, if any. + /// + public static string BuildRelative( + PathString pathBase = new PathString(), + PathString path = new PathString(), + QueryString query = new QueryString(), + FragmentString fragment = new FragmentString()) + { + string combinePath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/"; + return combinePath + query.ToString() + fragment.ToString(); + } + + /// + /// Combines the given URI components into a string that is properly encoded for use in HTTP headers. + /// Note that unicode in the HostString will be encoded as punycode. + /// + /// http, https, etc. + /// The host portion of the uri normally included in the Host header. This may include the port. + /// The first portion of the request path associated with application root. + /// The portion of the request path that identifies the requested resource. + /// The query, if any. + /// The fragment, if any. + /// + public static string BuildAbsolute( + string scheme, + HostString host, + PathString pathBase = new PathString(), + PathString path = new PathString(), + QueryString query = new QueryString(), + FragmentString fragment = new FragmentString()) + { + if (scheme == null) + { + throw new ArgumentNullException(nameof(scheme)); + } + + var combinedPath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/"; + + var encodedHost = host.ToString(); + var encodedQuery = query.ToString(); + var encodedFragment = fragment.ToString(); + + // PERF: Calculate string length to allocate correct buffer size for StringBuilder. + var length = scheme.Length + SchemeDelimiter.Length + encodedHost.Length + + combinedPath.Length + encodedQuery.Length + encodedFragment.Length; + + return new StringBuilder(length) + .Append(scheme) + .Append(SchemeDelimiter) + .Append(encodedHost) + .Append(combinedPath) + .Append(encodedQuery) + .Append(encodedFragment) + .ToString(); + } + + /// + /// Separates the given absolute URI string into components. Assumes no PathBase. + /// + /// A string representation of the uri. + /// http, https, etc. + /// The host portion of the uri normally included in the Host header. This may include the port. + /// The portion of the request path that identifies the requested resource. + /// The query, if any. + /// The fragment, if any. + public static void FromAbsolute( + string uri, + out string scheme, + out HostString host, + out PathString path, + out QueryString query, + out FragmentString fragment) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + path = new PathString(); + query = new QueryString(); + fragment = new FragmentString(); + var startIndex = uri.IndexOf(SchemeDelimiter); + + if (startIndex < 0) + { + throw new FormatException("No scheme delimiter in uri."); + } + + scheme = uri.Substring(0, startIndex); + + // PERF: Calculate the end of the scheme for next IndexOf + startIndex += SchemeDelimiter.Length; + + var searchIndex = -1; + var limit = uri.Length; + + if ((searchIndex = uri.IndexOf(Pound, startIndex)) >= 0 && searchIndex < limit) + { + fragment = FragmentString.FromUriComponent(uri.Substring(searchIndex)); + limit = searchIndex; + } + + if ((searchIndex = uri.IndexOf(QuestionMark, startIndex)) >= 0 && searchIndex < limit) + { + query = QueryString.FromUriComponent(uri.Substring(searchIndex, limit - searchIndex)); + limit = searchIndex; + } + + if ((searchIndex = uri.IndexOf(ForwardSlash, startIndex)) >= 0 && searchIndex < limit) + { + path = PathString.FromUriComponent(uri.Substring(searchIndex, limit - searchIndex)); + limit = searchIndex; + } + + host = HostString.FromUriComponent(uri.Substring(startIndex, limit - startIndex)); + } + + /// + /// Generates a string from the given absolute or relative Uri that is appropriately encoded for use in + /// HTTP headers. Note that a unicode host name will be encoded as punycode. + /// + /// The Uri to encode. + /// + public static string Encode(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (uri.IsAbsoluteUri) + { + return BuildAbsolute( + scheme: uri.Scheme, + host: HostString.FromUriComponent(uri), + pathBase: PathString.FromUriComponent(uri), + query: QueryString.FromUriComponent(uri), + fragment: FragmentString.FromUriComponent(uri)); + } + else + { + return uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); + } + } + + /// + /// Returns the combined components of the request URL in a fully escaped form suitable for use in HTTP headers + /// and other HTTP operations. + /// + /// The request to assemble the uri pieces from. + /// + public static string GetEncodedUrl(this HttpRequest request) + { + return BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path, request.QueryString); + } + /// + /// Returns the relative url + /// + /// The request to assemble the uri pieces from. + /// + public static string GetEncodedPathAndQuery(this HttpRequest request) + { + return BuildRelative(request.PathBase, request.Path, request.QueryString); + } + + /// + /// Returns the combined components of the request URL in a fully un-escaped form (except for the QueryString) + /// suitable only for display. This format should not be used in HTTP headers or other HTTP operations. + /// + /// The request to assemble the uri pieces from. + /// + public static string GetDisplayUrl(this HttpRequest request) + { + var host = request.Host.Value; + var pathBase = request.PathBase.Value; + var path = request.Path.Value; + var queryString = request.QueryString.Value; + + // PERF: Calculate string length to allocate correct buffer size for StringBuilder. + var length = request.Scheme.Length + SchemeDelimiter.Length + host.Length + + pathBase.Length + path.Length + queryString.Length; + + return new StringBuilder(length) + .Append(request.Scheme) + .Append(SchemeDelimiter) + .Append(host) + .Append(pathBase) + .Append(path) + .Append(queryString) + .ToString(); + } + } +} diff --git a/src/Http/Http.Extensions/src/baseline.netcore.json b/src/Http/Http.Extensions/src/baseline.netcore.json new file mode 100644 index 0000000000..286133ea54 --- /dev/null +++ b/src/Http/Http.Extensions/src/baseline.netcore.json @@ -0,0 +1,1699 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Http.Extensions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetTypedHeaders", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.Headers.RequestHeaders", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTypedHeaders", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.Headers.ResponseHeaders", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendList", + "Parameters": [ + { + "Name": "Headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.ResponseExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.SendFileResponseExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "file", + "Type": "Microsoft.Extensions.FileProviders.IFileInfo" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "file", + "Type": "Microsoft.Extensions.FileProviders.IFileInfo" + }, + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "count", + "Type": "System.Nullable" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "fileName", + "Type": "System.String" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + }, + { + "Name": "fileName", + "Type": "System.String" + }, + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "count", + "Type": "System.Nullable" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.SessionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "SetInt32", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetInt32", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Nullable", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetString", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetString", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get", + "Parameters": [ + { + "Name": "session", + "Type": "Microsoft.AspNetCore.Http.ISession" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Headers.RequestHeaders", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accept", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Accept", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AcceptCharset", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AcceptCharset", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AcceptEncoding", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AcceptEncoding", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AcceptLanguage", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AcceptLanguage", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CacheControl", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CacheControl", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.CacheControlHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentDisposition", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentDisposition", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentRange", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentRange", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentType", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Cookie", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Date", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Date", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expires", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expires", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Host", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HostString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Host", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.HostString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfMatch", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfMatch", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfModifiedSince", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfModifiedSince", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfNoneMatch", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfNoneMatch", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfRange", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfRange", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.RangeConditionHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IfUnmodifiedSince", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IfUnmodifiedSince", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LastModified", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LastModified", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Range", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.RangeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Range", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.RangeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Referer", + "Parameters": [], + "ReturnType": "System.Uri", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Referer", + "Parameters": [ + { + "Name": "value", + "Type": "System.Uri" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "GetList", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetList", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendList", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Headers.ResponseHeaders", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CacheControl", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.CacheControlHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CacheControl", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.CacheControlHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentDisposition", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentDisposition", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentRange", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentRange", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.ContentRangeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentType", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.MediaTypeHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Date", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Date", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ETag", + "Parameters": [], + "ReturnType": "Microsoft.Net.Http.Headers.EntityTagHeaderValue", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ETag", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.Net.Http.Headers.EntityTagHeaderValue" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expires", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expires", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LastModified", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LastModified", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Location", + "Parameters": [], + "ReturnType": "System.Uri", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Location", + "Parameters": [ + { + "Name": "value", + "Type": "System.Uri" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SetCookie", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SetCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "GetList", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetList", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendList", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "headers", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Extensions.HttpRequestMultipartExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetMultipartBoundary", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Extensions.QueryBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToQueryString", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.QueryString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetHashCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Equals", + "Parameters": [ + { + "Name": "obj", + "Type": "System.Object" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "parameters", + "Type": "System.Collections.Generic.IEnumerable>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CopyToAsync", + "Parameters": [ + { + "Name": "source", + "Type": "System.IO.Stream" + }, + { + "Name": "destination", + "Type": "System.IO.Stream" + }, + { + "Name": "count", + "Type": "System.Nullable" + }, + { + "Name": "cancel", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyToAsync", + "Parameters": [ + { + "Name": "source", + "Type": "System.IO.Stream" + }, + { + "Name": "destination", + "Type": "System.IO.Stream" + }, + { + "Name": "count", + "Type": "System.Nullable" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + }, + { + "Name": "cancel", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Extensions.UriHelper", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "BuildRelative", + "Parameters": [ + { + "Name": "pathBase", + "Type": "Microsoft.AspNetCore.Http.PathString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.PathString)" + }, + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.PathString)" + }, + { + "Name": "query", + "Type": "Microsoft.AspNetCore.Http.QueryString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.QueryString)" + }, + { + "Name": "fragment", + "Type": "Microsoft.AspNetCore.Http.FragmentString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.FragmentString)" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BuildAbsolute", + "Parameters": [ + { + "Name": "scheme", + "Type": "System.String" + }, + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Http.HostString" + }, + { + "Name": "pathBase", + "Type": "Microsoft.AspNetCore.Http.PathString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.PathString)" + }, + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.PathString)" + }, + { + "Name": "query", + "Type": "Microsoft.AspNetCore.Http.QueryString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.QueryString)" + }, + { + "Name": "fragment", + "Type": "Microsoft.AspNetCore.Http.FragmentString", + "DefaultValue": "default(Microsoft.AspNetCore.Http.FragmentString)" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromAbsolute", + "Parameters": [ + { + "Name": "uri", + "Type": "System.String" + }, + { + "Name": "scheme", + "Type": "System.String", + "Direction": "Out" + }, + { + "Name": "host", + "Type": "Microsoft.AspNetCore.Http.HostString", + "Direction": "Out" + }, + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString", + "Direction": "Out" + }, + { + "Name": "query", + "Type": "Microsoft.AspNetCore.Http.QueryString", + "Direction": "Out" + }, + { + "Name": "fragment", + "Type": "Microsoft.AspNetCore.Http.FragmentString", + "Direction": "Out" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Encode", + "Parameters": [ + { + "Name": "uri", + "Type": "System.Uri" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEncodedUrl", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEncodedPathAndQuery", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDisplayUrl", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs b/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs new file mode 100644 index 0000000000..1d01466284 --- /dev/null +++ b/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs @@ -0,0 +1,205 @@ +// 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.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Headers +{ + public class HeaderDictionaryTypeExtensionsTest + { + [Fact] + public void GetT_KnownTypeWithValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers[HeaderNames.ContentType] = "text/plain"; + + var result = context.Request.GetTypedHeaders().Get(HeaderNames.ContentType); + + var expected = new MediaTypeHeaderValue("text/plain"); + Assert.Equal(expected, result); + } + + [Fact] + public void GetT_KnownTypeWithMissingValue_Null() + { + var context = new DefaultHttpContext(); + + var result = context.Request.GetTypedHeaders().Get(HeaderNames.ContentType); + + Assert.Null(result); + } + + [Fact] + public void GetT_KnownTypeWithInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers[HeaderNames.ContentType] = "invalid"; + + var result = context.Request.GetTypedHeaders().Get(HeaderNames.ContentType); + + Assert.Null(result); + } + + [Fact] + public void GetT_UnknownTypeWithTryParseAndValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; + + var result = context.Request.GetTypedHeaders().Get("custom"); + Assert.NotNull(result); + } + + [Fact] + public void GetT_UnknownTypeWithTryParseAndInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "invalid"; + + var result = context.Request.GetTypedHeaders().Get("custom"); + Assert.Null(result); + } + + [Fact] + public void GetT_UnknownTypeWithTryParseAndMissingValue_Null() + { + var context = new DefaultHttpContext(); + + var result = context.Request.GetTypedHeaders().Get("custom"); + Assert.Null(result); + } + + [Fact] + public void GetT_UnknownTypeWithoutTryParse_Throws() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; + + Assert.Throws(() => context.Request.GetTypedHeaders().Get("custom")); + } + + [Fact] + public void GetListT_KnownTypeWithValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers[HeaderNames.Accept] = "text/plain; q=0.9, text/other, */*"; + + var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Accept); + + var expected = new[] { + new MediaTypeHeaderValue("text/plain", 0.9), + new MediaTypeHeaderValue("text/other"), + new MediaTypeHeaderValue("*/*"), + }.ToList(); + Assert.Equal(expected, result); + } + + [Fact] + public void GetListT_KnownTypeWithMissingValue_Null() + { + var context = new DefaultHttpContext(); + + var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Accept); + + Assert.Null(result); + } + + [Fact] + public void GetListT_KnownTypeWithInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers[HeaderNames.Accept] = "invalid"; + + var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Accept); + + Assert.Null(result); + } + + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; + + var results = context.Request.GetTypedHeaders().GetList("custom"); + Assert.NotNull(results); + Assert.Equal(new[] { new TestHeaderValue() }.ToList(), results); + } + + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "invalid"; + + var results = context.Request.GetTypedHeaders().GetList("custom"); + Assert.Null(results); + } + + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndMissingValue_Null() + { + var context = new DefaultHttpContext(); + + var results = context.Request.GetTypedHeaders().GetList("custom"); + Assert.Null(results); + } + + [Fact] + public void GetListT_UnknownTypeWithoutTryParseList_Throws() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; + + Assert.Throws(() => context.Request.GetTypedHeaders().GetList("custom")); + } + + public class TestHeaderValue + { + public static bool TryParse(string value, out TestHeaderValue result) + { + if (string.Equals("valid", value)) + { + result = new TestHeaderValue(); + return true; + } + result = null; + return false; + } + + public static bool TryParseList(IList values, out IList result) + { + var results = new List(); + foreach (var value in values) + { + if (string.Equals("valid", value)) + { + results.Add(new TestHeaderValue()); + } + } + if (results.Count > 0) + { + result = results; + return true; + } + result = null; + return false; + } + + public override bool Equals(object obj) + { + var other = obj as TestHeaderValue; + return other != null; + } + + public override int GetHashCode() + { + return 0; + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj new file mode 100644 index 0000000000..fae14d9f7a --- /dev/null +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -0,0 +1,13 @@ + + + + $(StandardTestTfms) + + + + + + + + + diff --git a/src/Http/Http.Extensions/test/QueryBuilderTests.cs b/src/Http/Http.Extensions/test/QueryBuilderTests.cs new file mode 100644 index 0000000000..7d15dd87bf --- /dev/null +++ b/src/Http/Http.Extensions/test/QueryBuilderTests.cs @@ -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; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + public class QueryBuilderTests + { + [Fact] + public void EmptyQuery_NoQuestionMark() + { + var builder = new QueryBuilder(); + Assert.Equal(string.Empty, builder.ToString()); + } + + [Fact] + public void AddSimple_NoEncoding() + { + var builder = new QueryBuilder(); + builder.Add("key", "value"); + Assert.Equal("?key=value", builder.ToString()); + } + + [Fact] + public void AddSpace_PercentEncoded() + { + var builder = new QueryBuilder(); + builder.Add("key", "value 1"); + Assert.Equal("?key=value%201", builder.ToString()); + } + + [Fact] + public void AddReservedCharacters_PercentEncoded() + { + var builder = new QueryBuilder(); + builder.Add("key&", "value#"); + Assert.Equal("?key%26=value%23", builder.ToString()); + } + + [Fact] + public void AddMultipleValues_AddedInOrder() + { + var builder = new QueryBuilder(); + builder.Add("key1", "value1"); + builder.Add("key2", "value2"); + builder.Add("key3", "value3"); + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); + } + + [Fact] + public void AddIEnumerableValues_AddedInOrder() + { + var builder = new QueryBuilder(); + builder.Add("key", new[] { "value1", "value2", "value3" }); + Assert.Equal("?key=value1&key=value2&key=value3", builder.ToString()); + } + + [Fact] + public void AddMultipleValuesViaConstructor_AddedInOrder() + { + var builder = new QueryBuilder(new[] + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2"), + new KeyValuePair("key3", "value3"), + }); + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); + } + + [Fact] + public void AddMultipleValuesViaInitializer_AddedInOrder() + { + var builder = new QueryBuilder() + { + { "key1", "value1" }, + { "key2", "value2" }, + { "key3", "value3" }, + }; + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); + } + + [Fact] + public void CopyViaConstructor_AddedInOrder() + { + var builder = new QueryBuilder() + { + { "key1", "value1" }, + { "key2", "value2" }, + { "key3", "value3" }, + }; + var builder1 = new QueryBuilder(builder); + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder1.ToString()); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ResponseExtensionTests.cs b/src/Http/Http.Extensions/test/ResponseExtensionTests.cs new file mode 100644 index 0000000000..ae6b147fd2 --- /dev/null +++ b/src/Http/Http.Extensions/test/ResponseExtensionTests.cs @@ -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.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Extensions +{ + public class ResponseExtensionTests + { + [Fact] + public void Clear_ResetsResponse() + { + var context = new DefaultHttpContext(); + context.Response.StatusCode = 201; + context.Response.Headers["custom"] = "value"; + context.Response.Body.Write(new byte[100], 0, 100); + + context.Response.Clear(); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Response.Headers["custom"].ToString()); + Assert.Equal(0, context.Response.Body.Length); + } + + [Fact] + public void Clear_AlreadyStarted_Throws() + { + var context = new DefaultHttpContext(); + context.Features.Set(new StartedResponseFeature()); + + Assert.Throws(() => context.Response.Clear()); + } + + private class StartedResponseFeature : IHttpResponseFeature + { + public Stream Body { get; set; } + + public bool HasStarted { get { return true; } } + + public IHeaderDictionary Headers { get; set; } + + public string ReasonPhrase { get; set; } + + public int StatusCode { get; set; } + + public void OnCompleted(Func callback, object state) + { + throw new NotImplementedException(); + } + + public void OnStarting(Func callback, object state) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs b/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs new file mode 100644 index 0000000000..f4c7c0f2a9 --- /dev/null +++ b/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Extensions.Tests +{ + public class SendFileResponseExtensionsTests + { + [Fact] + public Task SendFileWhenFileNotFoundThrows() + { + var response = new DefaultHttpContext().Response; + return Assert.ThrowsAsync(() => response.SendFileAsync("foo")); + } + + [Fact] + public async Task SendFileWorks() + { + var context = new DefaultHttpContext(); + var response = context.Response; + var fakeFeature = new FakeSendFileFeature(); + context.Features.Set(fakeFeature); + + await response.SendFileAsync("bob", 1, 3, CancellationToken.None); + + Assert.Equal("bob", fakeFeature.name); + Assert.Equal(1, fakeFeature.offset); + Assert.Equal(3, fakeFeature.length); + Assert.Equal(CancellationToken.None, fakeFeature.token); + } + + private class FakeSendFileFeature : IHttpSendFileFeature + { + public string name = null; + public long offset = 0; + public long? length = null; + public CancellationToken token; + + public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + this.name = path; + this.offset = offset; + this.length = length; + this.token = cancellation; + return Task.FromResult(0); + } + } + } +} diff --git a/src/Http/Http.Extensions/test/UriHelperTests.cs b/src/Http/Http.Extensions/test/UriHelperTests.cs new file mode 100644 index 0000000000..11b045af4f --- /dev/null +++ b/src/Http/Http.Extensions/test/UriHelperTests.cs @@ -0,0 +1,156 @@ +// 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.AspNetCore.Http.Extensions +{ + public class UriHelperTests + { + [Fact] + public void EncodeEmptyPartialUrl() + { + var result = UriHelper.BuildRelative(); + + Assert.Equal("/", result); + } + + [Fact] + public void EncodePartialUrl() + { + var result = UriHelper.BuildRelative(new PathString("/un?escaped/base"), new PathString("/un?escaped"), + new QueryString("?name=val%23ue"), new FragmentString("#my%20value")); + + Assert.Equal("/un%3Fescaped/base/un%3Fescaped?name=val%23ue#my%20value", result); + } + + [Fact] + public void EncodeEmptyFullUrl() + { + var result = UriHelper.BuildAbsolute("http", new HostString(string.Empty)); + + Assert.Equal("http:///", result); + } + + [Fact] + public void EncodeFullUrl() + { + var result = UriHelper.BuildAbsolute("http", new HostString("my.HoΨst:80"), new PathString("/un?escaped/base"), new PathString("/un?escaped"), + new QueryString("?name=val%23ue"), new FragmentString("#my%20value")); + + Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue#my%20value", result); + } + + [Fact] + public void GetEncodedUrlFromRequest() + { + var request = new DefaultHttpContext().Request; + request.Scheme = "http"; + request.Host = new HostString("my.HoΨst:80"); + request.PathBase = new PathString("/un?escaped/base"); + request.Path = new PathString("/un?escaped"); + request.QueryString = new QueryString("?name=val%23ue"); + + Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue", request.GetEncodedUrl()); + } + + [Fact] + public void GetDisplayUrlFromRequest() + { + var request = new DefaultHttpContext().Request; + request.Scheme = "http"; + request.Host = new HostString("my.HoΨst:80"); + request.PathBase = new PathString("/un?escaped/base"); + request.Path = new PathString("/un?escaped"); + request.QueryString = new QueryString("?name=val%23ue"); + + Assert.Equal("http://my.hoψst:80/un?escaped/base/un?escaped?name=val%23ue", request.GetDisplayUrl()); + } + + [Theory] + [InlineData("http://example.com", "http", "example.com", "", "", "")] + [InlineData("https://example.com", "https", "example.com", "", "", "")] + [InlineData("http://example.com/foo/bar", "http", "example.com", "/foo/bar", "", "")] + [InlineData("http://example.com/foo/bar?baz=1", "http", "example.com", "/foo/bar", "?baz=1", "")] + [InlineData("http://example.com/foo#col=2", "http", "example.com", "/foo", "", "#col=2")] + [InlineData("http://example.com/foo?bar=1#col=2", "http", "example.com", "/foo", "?bar=1", "#col=2")] + [InlineData("http://example.com?bar=1#col=2", "http", "example.com", "", "?bar=1", "#col=2")] + [InlineData("http://example.com#frag?stillfrag/stillfrag", "http", "example.com", "", "", "#frag?stillfrag/stillfrag")] + [InlineData("http://example.com?q/stillq#frag?stillfrag/stillfrag", "http", "example.com", "", "?q/stillq", "#frag?stillfrag/stillfrag")] + [InlineData("http://example.com/fo%23o#col=2", "http", "example.com", "/fo#o", "", "#col=2")] + [InlineData("http://example.com/fo%3Fo#col=2", "http", "example.com", "/fo?o", "", "#col=2")] + [InlineData("ftp://example.com/", "ftp", "example.com", "/", "", "")] + [InlineData("https://127.0.0.0:80/bar", "https", "127.0.0.0:80", "/bar", "", "")] + [InlineData("http://[1080:0:0:0:8:800:200C:417A]/index.html", "http", "[1080:0:0:0:8:800:200C:417A]", "/index.html", "", "")] + [InlineData("http://example.com///", "http", "example.com", "///", "", "")] + public void FromAbsoluteUriParsingChecks( + string uri, + string expectedScheme, + string expectedHost, + string expectedPath, + string expectedQuery, + string expectedFragment) + { + string scheme = null; + var host = new HostString(); + var path = new PathString(); + var query = new QueryString(); + var fragment = new FragmentString(); + UriHelper.FromAbsolute(uri, out scheme, out host, out path, out query, out fragment); + + Assert.Equal(scheme, expectedScheme); + Assert.Equal(host, new HostString(expectedHost)); + Assert.Equal(path, new PathString(expectedPath)); + Assert.Equal(query, new QueryString(expectedQuery)); + Assert.Equal(fragment, new FragmentString(expectedFragment)); + } + + [Fact] + public void FromAbsoluteToBuildAbsolute() + { + var scheme = "http"; + var host = new HostString("example.com"); + var path = new PathString("/index.html"); + var query = new QueryString("?foo=1"); + var fragment = new FragmentString("#col=1"); + var request = UriHelper.BuildAbsolute(scheme, host, path:path, query:query, fragment:fragment); + + string resScheme = null; + var resHost = new HostString(); + var resPath = new PathString(); + var resQuery = new QueryString(); + var resFragment = new FragmentString(); + UriHelper.FromAbsolute(request, out resScheme, out resHost, out resPath, out resQuery, out resFragment); + + Assert.Equal(scheme, resScheme); + Assert.Equal(host, resHost); + Assert.Equal(path, resPath); + Assert.Equal(query, resQuery); + Assert.Equal(fragment, resFragment); + } + + [Fact] + public void BuildAbsoluteNullInputThrowsArgumentNullException() + { + var resHost = new HostString(); + var resPath = new PathString(); + var resQuery = new QueryString(); + var resFragment = new FragmentString(); + Assert.Throws(() => UriHelper.BuildAbsolute(null, resHost, resPath, resPath, resQuery, resFragment)); + + } + + [Fact] + public void FromAbsoluteNullInputThrowsArgumentNullException() + { + string resScheme = null; + var resHost = new HostString(); + var resPath = new PathString(); + var resQuery = new QueryString(); + var resFragment = new FragmentString(); + Assert.Throws(() => UriHelper.FromAbsolute(null, out resScheme, out resHost, out resPath, out resQuery, out resFragment)); + + } + } +} diff --git a/src/Http/Http.Features/src/Authentication/AuthenticateContext.cs b/src/Http/Http.Features/src/Authentication/AuthenticateContext.cs new file mode 100644 index 0000000000..e73061667b --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/AuthenticateContext.cs @@ -0,0 +1,69 @@ +// 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; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class AuthenticateContext + { + public AuthenticateContext(string authenticationScheme) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + AuthenticationScheme = authenticationScheme; + } + + public string AuthenticationScheme { get; } + + public bool Accepted { get; private set; } + + public ClaimsPrincipal Principal { get; private set; } + + public IDictionary Properties { get; private set; } + + public IDictionary Description { get; private set; } + + public Exception Error { get; private set; } + + public virtual void Authenticated(ClaimsPrincipal principal, IDictionary properties, IDictionary description) + { + Accepted = true; + + Principal = principal; + Properties = properties; + Description = description; + + // Set defaults for fields we don't use in case multiple handlers modified the context. + Error = null; + } + + public virtual void NotAuthenticated() + { + Accepted = true; + + // Set defaults for fields we don't use in case multiple handlers modified the context. + Description = null; + Error = null; + Principal = null; + Properties = null; + } + + public virtual void Failed(Exception error) + { + Accepted = true; + + Error = error; + + // Set defaults for fields we don't use in case multiple handlers modified the context. + Description = null; + Principal = null; + Properties = null; + } + } +} diff --git a/src/Http/Http.Features/src/Authentication/ChallengeBehavior.cs b/src/Http/Http.Features/src/Authentication/ChallengeBehavior.cs new file mode 100644 index 0000000000..549d51132a --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/ChallengeBehavior.cs @@ -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.AspNetCore.Http.Features.Authentication +{ + public enum ChallengeBehavior + { + Automatic, + Unauthorized, + Forbidden + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/ChallengeContext.cs b/src/Http/Http.Features/src/Authentication/ChallengeContext.cs new file mode 100644 index 0000000000..c0fe470806 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/ChallengeContext.cs @@ -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 System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class ChallengeContext + { + public ChallengeContext(string authenticationScheme) + : this(authenticationScheme, properties: null, behavior: ChallengeBehavior.Automatic) + { + } + + public ChallengeContext(string authenticationScheme, IDictionary properties, ChallengeBehavior behavior) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + AuthenticationScheme = authenticationScheme; + Properties = properties ?? new Dictionary(StringComparer.Ordinal); + Behavior = behavior; + } + + public string AuthenticationScheme { get; } + + public ChallengeBehavior Behavior { get; } + + public IDictionary Properties { get; } + + public bool Accepted { get; private set; } + + public void Accept() + { + Accepted = true; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/DescribeSchemesContext.cs b/src/Http/Http.Features/src/Authentication/DescribeSchemesContext.cs new file mode 100644 index 0000000000..b25c2c979a --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/DescribeSchemesContext.cs @@ -0,0 +1,27 @@ +// 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; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class DescribeSchemesContext + { + private List> _results; + + public DescribeSchemesContext() + { + _results = new List>(); + } + + public IEnumerable> Results + { + get { return _results; } + } + + public void Accept(IDictionary description) + { + _results.Add(description); + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/IAuthenticationHandler.cs b/src/Http/Http.Features/src/Authentication/IAuthenticationHandler.cs new file mode 100644 index 0000000000..3b72364182 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/IAuthenticationHandler.cs @@ -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.Http.Features.Authentication +{ + public interface IAuthenticationHandler + { + void GetDescriptions(DescribeSchemesContext context); + + Task AuthenticateAsync(AuthenticateContext context); + + Task ChallengeAsync(ChallengeContext context); + + Task SignInAsync(SignInContext context); + + Task SignOutAsync(SignOutContext context); + } +} diff --git a/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs b/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs new file mode 100644 index 0000000000..279d6904f0 --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs @@ -0,0 +1,16 @@ +// 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.Features.Authentication +{ + public interface IHttpAuthenticationFeature + { + ClaimsPrincipal User { get; set; } + + [Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")] + IAuthenticationHandler Handler { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/SignInContext.cs b/src/Http/Http.Features/src/Authentication/SignInContext.cs new file mode 100644 index 0000000000..f04dade51b --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/SignInContext.cs @@ -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; +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class SignInContext + { + public SignInContext(string authenticationScheme, ClaimsPrincipal principal, IDictionary properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + AuthenticationScheme = authenticationScheme; + Principal = principal; + Properties = properties ?? new Dictionary(StringComparer.Ordinal); + } + + public string AuthenticationScheme { get; } + + public ClaimsPrincipal Principal { get; } + + public IDictionary Properties { get; } + + public bool Accepted { get; private set; } + + public void Accept() + { + Accepted = true; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/Authentication/SignOutContext.cs b/src/Http/Http.Features/src/Authentication/SignOutContext.cs new file mode 100644 index 0000000000..c752f057df --- /dev/null +++ b/src/Http/Http.Features/src/Authentication/SignOutContext.cs @@ -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.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class SignOutContext + { + public SignOutContext(string authenticationScheme, IDictionary properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + AuthenticationScheme = authenticationScheme; + Properties = properties ?? new Dictionary(StringComparer.Ordinal); + } + + public string AuthenticationScheme { get; } + + public IDictionary Properties { get; } + + public bool Accepted { get; private set; } + + public void Accept() + { + Accepted = true; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/CookieOptions.cs b/src/Http/Http.Features/src/CookieOptions.cs new file mode 100644 index 0000000000..27141a32f2 --- /dev/null +++ b/src/Http/Http.Features/src/CookieOptions.cs @@ -0,0 +1,69 @@ +// 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.Http +{ + /// + /// Options used to create a new cookie. + /// + public class CookieOptions + { + /// + /// Creates a default cookie with a path of '/'. + /// + public CookieOptions() + { + Path = "/"; + } + + /// + /// Gets or sets the domain to associate the cookie with. + /// + /// The domain to associate the cookie with. + public string Domain { get; set; } + + /// + /// Gets or sets the cookie path. + /// + /// The cookie path. + public string Path { get; set; } + + /// + /// Gets or sets the expiration date and time for the cookie. + /// + /// The expiration date and time for the cookie. + public DateTimeOffset? Expires { get; set; } + + /// + /// Gets or sets a value that indicates whether to transmit the cookie using Secure Sockets Layer (SSL)--that is, over HTTPS only. + /// + /// true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false. + public bool Secure { get; set; } + + /// + /// Gets or sets the value for the SameSite attribute of the cookie. The default value is + /// + /// The representing the enforcement mode of the cookie. + public SameSiteMode SameSite { get; set; } = SameSiteMode.Lax; + + /// + /// Gets or sets a value that indicates whether a cookie is accessible by client-side script. + /// + /// true if a cookie must not be accessible by client-side script; otherwise, false. + public bool HttpOnly { get; set; } + + /// + /// Gets or sets the max-age for the cookie. + /// + /// The max-age date and time for the cookie. + public TimeSpan? MaxAge { get; set; } + + /// + /// 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. + /// + public bool IsEssential { get; set; } + } +} diff --git a/src/Http/Http.Features/src/FeatureCollection.cs b/src/Http/Http.Features/src/FeatureCollection.cs new file mode 100644 index 0000000000..e79ecfee22 --- /dev/null +++ b/src/Http/Http.Features/src/FeatureCollection.cs @@ -0,0 +1,119 @@ +// 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; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FeatureCollection : IFeatureCollection + { + private static KeyComparer FeatureKeyComparer = new KeyComparer(); + private readonly IFeatureCollection _defaults; + private IDictionary _features; + private volatile int _containerRevision; + + public FeatureCollection() + { + } + + public FeatureCollection(IFeatureCollection defaults) + { + _defaults = defaults; + } + + public virtual int Revision + { + get { return _containerRevision + (_defaults?.Revision ?? 0); } + } + + public bool IsReadOnly { get { return false; } } + + public object this[Type key] + { + get + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + object result; + return _features != null && _features.TryGetValue(key, out result) ? result : _defaults?[key]; + } + set + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + if (_features != null && _features.Remove(key)) + { + _containerRevision++; + } + return; + } + + if (_features == null) + { + _features = new Dictionary(); + } + _features[key] = value; + _containerRevision++; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator> GetEnumerator() + { + if (_features != null) + { + foreach (var pair in _features) + { + yield return pair; + } + } + + if (_defaults != null) + { + // Don't return features masked by the wrapper. + foreach (var pair in _features == null ? _defaults : _defaults.Except(_features, FeatureKeyComparer)) + { + yield return pair; + } + } + } + + public TFeature Get() + { + return (TFeature)this[typeof(TFeature)]; + } + + public void Set(TFeature instance) + { + this[typeof(TFeature)] = instance; + } + + private class KeyComparer : IEqualityComparer> + { + public bool Equals(KeyValuePair x, KeyValuePair y) + { + return x.Key.Equals(y.Key); + } + + public int GetHashCode(KeyValuePair obj) + { + return obj.Key.GetHashCode(); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/FeatureReference.cs b/src/Http/Http.Features/src/FeatureReference.cs new file mode 100644 index 0000000000..5016602123 --- /dev/null +++ b/src/Http/Http.Features/src/FeatureReference.cs @@ -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. + +namespace Microsoft.AspNetCore.Http.Features +{ + public struct FeatureReference + { + private T _feature; + private int _revision; + + private FeatureReference(T feature, int revision) + { + _feature = feature; + _revision = revision; + } + + public static readonly FeatureReference Default = new FeatureReference(default(T), -1); + + public T Fetch(IFeatureCollection features) + { + if (_revision == features.Revision) + { + return _feature; + } + _feature = (T)features[typeof(T)]; + _revision = features.Revision; + return _feature; + } + + public T Update(IFeatureCollection features, T feature) + { + features[typeof(T)] = feature; + _feature = feature; + _revision = features.Revision; + return feature; + } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/FeatureReferences.cs b/src/Http/Http.Features/src/FeatureReferences.cs new file mode 100644 index 0000000000..38bd2ec27a --- /dev/null +++ b/src/Http/Http.Features/src/FeatureReferences.cs @@ -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; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Http.Features +{ + public struct FeatureReferences + { + public FeatureReferences(IFeatureCollection collection) + { + Collection = collection; + Cache = default(TCache); + Revision = collection.Revision; + } + + public IFeatureCollection Collection { get; private set; } + public int Revision { get; private set; } + + // cache is a public field because the code calling Fetch must + // be able to pass ref values that "dot through" the TCache struct memory, + // if it was a Property then that getter would return a copy of the memory + // preventing the use of "ref" + public TCache Cache; + + // Careful with modifications to the Fetch method; it is carefully constructed for inlining + // See: https://github.com/aspnet/HttpAbstractions/pull/704 + // This method is 59 IL bytes and at inline call depth 3 from accessing a property. + // This combination is enough for the jit to consider it an "unprofitable inline" + // Aggressively inlining it causes the entire call chain to dissolve: + // + // This means this call graph: + // + // HttpResponse.Headers -> Response.HttpResponseFeature -> Fetch -> Fetch -> Revision + // -> Collection -> Collection + // -> Collection.Revision + // Has 6 calls eliminated and becomes just: -> UpdateCached + // + // HttpResponse.Headers -> Collection.Revision + // -> UpdateCached (not called on fast path) + // + // As this is inlined at the callsite we want to keep the method small, so it only detects + // if a reset or update is required and all the reset and update logic is pushed to UpdateCached. + // + // Generally Fetch is called at a ratio > x4 of UpdateCached so this is a large gain + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TFeature Fetch( + ref TFeature cached, + TState state, + Func factory) where TFeature : class + { + var flush = false; + var revision = Collection.Revision; + if (Revision != revision) + { + // Clear cached value to force call to UpdateCached + cached = null; + // Collection changed, clear whole feature cache + flush = true; + } + + return cached ?? UpdateCached(ref cached, state, factory, revision, flush); + } + + // Update and cache clearing logic, when the fast-path in Fetch isn't applicable + private TFeature UpdateCached(ref TFeature cached, TState state, Func factory, int revision, bool flush) where TFeature : class + { + if (flush) + { + // Collection detected as changed, clear cache + Cache = default(TCache); + } + + cached = Collection.Get(); + if (cached == null) + { + // Item not in collection, create it with factory + cached = factory(state); + // Add item to IFeatureCollection + Collection.Set(cached); + // Revision changed by .Set, update revision to new value + Revision = Collection.Revision; + } + else if (flush) + { + // Cache was cleared, but item retrived from current Collection for version + // so use passed in revision rather than making another virtual call + Revision = revision; + } + + return cached; + } + + public TFeature Fetch(ref TFeature cached, Func factory) + where TFeature : class => Fetch(ref cached, Collection, factory); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IFeatureCollection.cs b/src/Http/Http.Features/src/IFeatureCollection.cs new file mode 100644 index 0000000000..f7b23ed16f --- /dev/null +++ b/src/Http/Http.Features/src/IFeatureCollection.cs @@ -0,0 +1,45 @@ +// 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.AspNetCore.Http.Features +{ + /// + /// Represents a collection of HTTP features. + /// + public interface IFeatureCollection : IEnumerable> + { + /// + /// Indicates if the collection can be modified. + /// + bool IsReadOnly { get; } + + /// + /// Incremented for each modification and can be used to verify cached results. + /// + int Revision { get; } + + /// + /// Gets or sets a given feature. Setting a null value removes the feature. + /// + /// + /// The requested feature, or null if it is not present. + object this[Type key] { get; set; } + + /// + /// Retrieves the requested feature from the collection. + /// + /// The feature key. + /// The requested feature, or null if it is not present. + TFeature Get(); + + /// + /// Sets the given feature in the collection. + /// + /// The feature key. + /// The feature value. + void Set(TFeature instance); + } +} diff --git a/src/Http/Http.Features/src/IFormCollection.cs b/src/Http/Http.Features/src/IFormCollection.cs new file mode 100644 index 0000000000..237d311ae8 --- /dev/null +++ b/src/Http/Http.Features/src/IFormCollection.cs @@ -0,0 +1,94 @@ +// 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 Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents the parsed form values sent with the HttpRequest. + /// + public interface IFormCollection : IEnumerable> + { + /// + /// Gets the number of elements contained in the . + /// + /// + /// The number of elements contained in the . + /// + int Count { get; } + + /// + /// Gets an containing the keys of the + /// . + /// + /// + /// An containing the keys of the object + /// that implements . + /// + ICollection Keys { get; } + + /// + /// Determines whether the contains an element + /// with the specified key. + /// + /// + /// The key to locate in the . + /// + /// + /// true if the contains an element with + /// the key; otherwise, false. + /// + /// + /// key is null. + /// + bool ContainsKey(string key); + + /// + /// Gets the value associated with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the object that implements contains + /// an element with the specified key; otherwise, false. + /// + /// + /// key is null. + /// + bool TryGetValue(string key, out StringValues value); + + /// + /// Gets the value with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The element with the specified key, or StringValues.Empty if the key is not present. + /// + /// + /// key is null. + /// + /// + /// has a different indexer contract than + /// , as it will return StringValues.Empty for missing entries + /// rather than throwing an Exception. + /// + StringValues this[string key] { get; } + + /// + /// The file collection sent with the request. + /// + /// The files included with the request. + IFormFileCollection Files { get; } + } +} diff --git a/src/Http/Http.Features/src/IFormFeature.cs b/src/Http/Http.Features/src/IFormFeature.cs new file mode 100644 index 0000000000..f10ed47b80 --- /dev/null +++ b/src/Http/Http.Features/src/IFormFeature.cs @@ -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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IFormFeature + { + /// + /// Indicates if the request has a supported form content-type. + /// + bool HasFormContentType { get; } + + /// + /// The parsed form, if any. + /// + IFormCollection Form { get; set; } + + /// + /// Parses the request body as a form. + /// + /// + IFormCollection ReadForm(); + + /// + /// Parses the request body as a form. + /// + /// + /// + Task ReadFormAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Http/Http.Features/src/IFormFile.cs b/src/Http/Http.Features/src/IFormFile.cs new file mode 100644 index 0000000000..f52e71bfee --- /dev/null +++ b/src/Http/Http.Features/src/IFormFile.cs @@ -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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents a file sent with the HttpRequest. + /// + public interface IFormFile + { + /// + /// Gets the raw Content-Type header of the uploaded file. + /// + string ContentType { get; } + + /// + /// Gets the raw Content-Disposition header of the uploaded file. + /// + string ContentDisposition { get; } + + /// + /// Gets the header dictionary of the uploaded file. + /// + IHeaderDictionary Headers { get; } + + /// + /// Gets the file length in bytes. + /// + long Length { get; } + + /// + /// Gets the form field name from the Content-Disposition header. + /// + string Name { get; } + + /// + /// Gets the file name from the Content-Disposition header. + /// + string FileName { get; } + + /// + /// Opens the request stream for reading the uploaded file. + /// + Stream OpenReadStream(); + + /// + /// Copies the contents of the uploaded file to the stream. + /// + /// The stream to copy the file contents to. + void CopyTo(Stream target); + + /// + /// Asynchronously copies the contents of the uploaded file to the stream. + /// + /// The stream to copy the file contents to. + /// + Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Http/Http.Features/src/IFormFileCollection.cs b/src/Http/Http.Features/src/IFormFileCollection.cs new file mode 100644 index 0000000000..e66c96e05d --- /dev/null +++ b/src/Http/Http.Features/src/IFormFileCollection.cs @@ -0,0 +1,19 @@ +// 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; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents the collection of files sent with the HttpRequest. + /// + public interface IFormFileCollection : IReadOnlyList + { + IFormFile this[string name] { get; } + + IFormFile GetFile(string name); + + IReadOnlyList GetFiles(string name); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHeaderDictionary.cs b/src/Http/Http.Features/src/IHeaderDictionary.cs new file mode 100644 index 0000000000..dfde3f33e3 --- /dev/null +++ b/src/Http/Http.Features/src/IHeaderDictionary.cs @@ -0,0 +1,26 @@ +// 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 Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents HttpRequest and HttpResponse headers + /// + public interface IHeaderDictionary : IDictionary + { + /// + /// IHeaderDictionary has a different indexer contract than IDictionary, where it will return StringValues.Empty for missing entries. + /// + /// + /// The stored value, or StringValues.Empty if the key is not present. + new StringValues this[string key] { get; set; } + + /// + /// Strongly typed access to the Content-Length header. Implementations must keep this in sync with the string representation. + /// + long? ContentLength { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IHttpBodyControlFeature.cs b/src/Http/Http.Features/src/IHttpBodyControlFeature.cs new file mode 100644 index 0000000000..3f61be9788 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpBodyControlFeature.cs @@ -0,0 +1,16 @@ +// 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.Features +{ + /// + /// Controls the IO behavior for the and + /// + public interface IHttpBodyControlFeature + { + /// + /// Gets or sets a value that controls whether synchronous IO is allowed for the and + /// + bool AllowSynchronousIO { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IHttpBufferingFeature.cs b/src/Http/Http.Features/src/IHttpBufferingFeature.cs new file mode 100644 index 0000000000..fae7f3d0ff --- /dev/null +++ b/src/Http/Http.Features/src/IHttpBufferingFeature.cs @@ -0,0 +1,11 @@ +// 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.Features +{ + public interface IHttpBufferingFeature + { + void DisableRequestBuffering(); + void DisableResponseBuffering(); + } +} diff --git a/src/Http/Http.Features/src/IHttpConnectionFeature.cs b/src/Http/Http.Features/src/IHttpConnectionFeature.cs new file mode 100644 index 0000000000..932e9bfe2c --- /dev/null +++ b/src/Http/Http.Features/src/IHttpConnectionFeature.cs @@ -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.Net; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Information regarding the TCP/IP connection carrying the request. + /// + public interface IHttpConnectionFeature + { + /// + /// The unique identifier for the connection the request was received on. This is primarily for diagnostic purposes. + /// + string ConnectionId { get; set; } + + /// + /// The IPAddress of the client making the request. Note this may be for a proxy rather than the end user. + /// + IPAddress RemoteIpAddress { get; set; } + + /// + /// The local IPAddress on which the request was received. + /// + IPAddress LocalIpAddress { get; set; } + + /// + /// The remote port of the client making the request. + /// + int RemotePort { get; set; } + + /// + /// The local port on which the request was received. + /// + int LocalPort { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs b/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs new file mode 100644 index 0000000000..c02000c72a --- /dev/null +++ b/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs @@ -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. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Feature to inspect and modify the maximum request body size for a single request. + /// + public interface IHttpMaxRequestBodySizeFeature + { + /// + /// Indicates whether is read-only. + /// If true, this could mean that the request body has already been read from + /// or that was called. + /// + bool IsReadOnly { get; } + + /// + /// The maximum allowed size of the current request body in bytes. + /// When set to null, the maximum request body size is unlimited. + /// This cannot be modified after the reading the request body has started. + /// This limit does not affect upgraded connections which are always unlimited. + /// + /// + /// Defaults to the server's global max request body size limit. + /// + long? MaxRequestBodySize { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpRequestFeature.cs b/src/Http/Http.Features/src/IHttpRequestFeature.cs new file mode 100644 index 0000000000..5a84221b57 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpRequestFeature.cs @@ -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.IO; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Contains the details of a given request. These properties should all be mutable. + /// None of these properties should ever be set to null. + /// + public interface IHttpRequestFeature + { + /// + /// The HTTP-version as defined in RFC 7230. E.g. "HTTP/1.1" + /// + string Protocol { get; set; } + + /// + /// The request uri scheme. E.g. "http" or "https". Note this value is not included + /// in the original request, it is inferred by checking if the transport used a TLS + /// connection or not. + /// + string Scheme { get; set; } + + /// + /// The request method as defined in RFC 7230. E.g. "GET", "HEAD", "POST", etc.. + /// + string Method { get; set; } + + /// + /// The first portion of the request path associated with application root. The value + /// is un-escaped. The value may be string.Empty. + /// + string PathBase { get; set; } + + /// + /// The portion of the request path that identifies the requested resource. The value + /// is un-escaped. The value may be string.Empty if contains the + /// full path. + /// + string Path { get; set; } + + /// + /// The query portion of the request-target as defined in RFC 7230. The value + /// may be string.Empty. If not empty then the leading '?' will be included. The value + /// is in its original form, without un-escaping. + /// + string QueryString { get; set; } + + /// + /// The request target as it was sent in the HTTP request. This property contains the + /// raw path and full query, as well as other request targets such as * for OPTIONS + /// requests (https://tools.ietf.org/html/rfc7230#section-5.3). + /// + /// + /// This property is not used internally for routing or authorization decisions. It has not + /// been UrlDecoded and care should be taken in its use. + /// + string RawTarget { get; set; } + + /// + /// Headers included in the request, aggregated by header name. The values are not split + /// or merged across header lines. E.g. The following headers: + /// HeaderA: value1, value2 + /// HeaderA: value3 + /// Result in Headers["HeaderA"] = { "value1, value2", "value3" } + /// + IHeaderDictionary Headers { get; set; } + + /// + /// A representing the request body, if any. Stream.Null may be used + /// to represent an empty request body. + /// + Stream Body { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs b/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs new file mode 100644 index 0000000000..9b0b5201d7 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs @@ -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. + +using System; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Feature to identify a request. + /// + public interface IHttpRequestIdentifierFeature + { + /// + /// Identifier to trace a request. + /// + string TraceIdentifier { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs b/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs new file mode 100644 index 0000000000..1bdac15766 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs @@ -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.Threading; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IHttpRequestLifetimeFeature + { + /// + /// A that fires if the request is aborted and + /// the application should cease processing. The token will not fire if the request + /// completes successfully. + /// + CancellationToken RequestAborted { get; set; } + + /// + /// Forcefully aborts the request if it has not already completed. This will result in + /// RequestAborted being triggered. + /// + void Abort(); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpResponseFeature.cs b/src/Http/Http.Features/src/IHttpResponseFeature.cs new file mode 100644 index 0000000000..9d3b957efb --- /dev/null +++ b/src/Http/Http.Features/src/IHttpResponseFeature.cs @@ -0,0 +1,59 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Represents the fields and state of an HTTP response. + /// + public interface IHttpResponseFeature + { + /// + /// The status-code as defined in RFC 7230. The default value is 200. + /// + int StatusCode { get; set; } + + /// + /// The reason-phrase as defined in RFC 7230. Note this field is no longer supported by HTTP/2. + /// + string ReasonPhrase { get; set; } + + /// + /// The response headers to send. Headers with multiple values will be emitted as multiple headers. + /// + IHeaderDictionary Headers { get; set; } + + /// + /// The for writing the response body. + /// + Stream Body { get; set; } + + /// + /// Indicates if the response has started. If true, the , + /// , and are now immutable, and + /// OnStarting should no longer be called. + /// + bool HasStarted { get; } + + /// + /// Registers a callback to be invoked just before the response starts. This is the + /// last chance to modify the , , or + /// . + /// + /// The callback to invoke when starting the response. + /// The state to pass into the callback. + void OnStarting(Func callback, object state); + + /// + /// Registers a callback to be invoked after a response has fully completed. This is + /// intended for resource cleanup. + /// + /// The callback to invoke after the response has completed. + /// The state to pass into the callback. + void OnCompleted(Func callback, object state); + } +} diff --git a/src/Http/Http.Features/src/IHttpSendFileFeature.cs b/src/Http/Http.Features/src/IHttpSendFileFeature.cs new file mode 100644 index 0000000000..1e2684130f --- /dev/null +++ b/src/Http/Http.Features/src/IHttpSendFileFeature.cs @@ -0,0 +1,26 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Provides an efficient mechanism for transferring files from disk to the network. + /// + public interface IHttpSendFileFeature + { + /// + /// Sends the requested file in the response body. This may bypass the IHttpResponseFeature.Body + /// . A response may include multiple writes. + /// + /// The full disk path to the file. + /// The offset in the file to start at. + /// The number of bytes to send, or null to send the remainder of the file. + /// A used to abort the transmission. + /// + Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpUpgradeFeature.cs b/src/Http/Http.Features/src/IHttpUpgradeFeature.cs new file mode 100644 index 0000000000..e434fe0b97 --- /dev/null +++ b/src/Http/Http.Features/src/IHttpUpgradeFeature.cs @@ -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.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IHttpUpgradeFeature + { + /// + /// Indicates if the server can upgrade this request to an opaque, bidirectional stream. + /// + bool IsUpgradableRequest { get; } + + /// + /// Attempt to upgrade the request to an opaque, bidirectional stream. The response status code + /// and headers need to be set before this is invoked. Check + /// before invoking. + /// + /// + Task UpgradeAsync(); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IHttpWebSocketFeature.cs b/src/Http/Http.Features/src/IHttpWebSocketFeature.cs new file mode 100644 index 0000000000..c1d116126a --- /dev/null +++ b/src/Http/Http.Features/src/IHttpWebSocketFeature.cs @@ -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.Net.WebSockets; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IHttpWebSocketFeature + { + /// + /// Indicates if this is a WebSocket upgrade request. + /// + bool IsWebSocketRequest { get; } + + /// + /// Attempts to upgrade the request to a . Check + /// before invoking this. + /// + /// + /// + Task AcceptAsync(WebSocketAcceptContext context); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IItemsFeature.cs b/src/Http/Http.Features/src/IItemsFeature.cs new file mode 100644 index 0000000000..bea03e466c --- /dev/null +++ b/src/Http/Http.Features/src/IItemsFeature.cs @@ -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. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IItemsFeature + { + IDictionary Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IQueryCollection.cs b/src/Http/Http.Features/src/IQueryCollection.cs new file mode 100644 index 0000000000..5d45ad2493 --- /dev/null +++ b/src/Http/Http.Features/src/IQueryCollection.cs @@ -0,0 +1,88 @@ +// 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 Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents the HttpRequest query string collection + /// + public interface IQueryCollection : IEnumerable> + { + /// + /// Gets the number of elements contained in the . + /// + /// + /// The number of elements contained in the . + /// + int Count { get; } + + /// + /// Gets an containing the keys of the + /// . + /// + /// + /// An containing the keys of the object + /// that implements . + /// + ICollection Keys { get; } + + /// + /// Determines whether the contains an element + /// with the specified key. + /// + /// + /// The key to locate in the . + /// + /// + /// true if the contains an element with + /// the key; otherwise, false. + /// + /// + /// key is null. + /// + bool ContainsKey(string key); + + /// + /// Gets the value associated with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the object that implements contains + /// an element with the specified key; otherwise, false. + /// + /// + /// key is null. + /// + bool TryGetValue(string key, out StringValues value); + + /// + /// Gets the value with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The element with the specified key, or StringValues.Empty if the key is not present. + /// + /// + /// key is null. + /// + /// + /// has a different indexer contract than + /// , as it will return StringValues.Empty for missing entries + /// rather than throwing an Exception. + /// + StringValues this[string key] { get; } + } +} diff --git a/src/Http/Http.Features/src/IQueryFeature.cs b/src/Http/Http.Features/src/IQueryFeature.cs new file mode 100644 index 0000000000..4f307f8f90 --- /dev/null +++ b/src/Http/Http.Features/src/IQueryFeature.cs @@ -0,0 +1,10 @@ +// 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.Features +{ + public interface IQueryFeature + { + IQueryCollection Query { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IRequestCookieCollection.cs b/src/Http/Http.Features/src/IRequestCookieCollection.cs new file mode 100644 index 0000000000..6e9444ac8f --- /dev/null +++ b/src/Http/Http.Features/src/IRequestCookieCollection.cs @@ -0,0 +1,87 @@ +// 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; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents the HttpRequest cookie collection + /// + public interface IRequestCookieCollection : IEnumerable> + { + /// + /// Gets the number of elements contained in the . + /// + /// + /// The number of elements contained in the . + /// + int Count { get; } + + /// + /// Gets an containing the keys of the + /// . + /// + /// + /// An containing the keys of the object + /// that implements . + /// + ICollection Keys { get; } + + /// + /// Determines whether the contains an element + /// with the specified key. + /// + /// + /// The key to locate in the . + /// + /// + /// true if the contains an element with + /// the key; otherwise, false. + /// + /// + /// key is null. + /// + bool ContainsKey(string key); + + /// + /// Gets the value associated with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the object that implements contains + /// an element with the specified key; otherwise, false. + /// + /// + /// key is null. + /// + bool TryGetValue(string key, out string value); + + /// + /// Gets the value with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The element with the specified key, or string.Empty if the key is not present. + /// + /// + /// key is null. + /// + /// + /// has a different indexer contract than + /// , as it will return string.Empty for missing entries + /// rather than throwing an Exception. + /// + string this[string key] { get; } + } +} diff --git a/src/Http/Http.Features/src/IRequestCookiesFeature.cs b/src/Http/Http.Features/src/IRequestCookiesFeature.cs new file mode 100644 index 0000000000..55ba603642 --- /dev/null +++ b/src/Http/Http.Features/src/IRequestCookiesFeature.cs @@ -0,0 +1,10 @@ +// 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.Features +{ + public interface IRequestCookiesFeature + { + IRequestCookieCollection Cookies { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IResponseCookies.cs b/src/Http/Http.Features/src/IResponseCookies.cs new file mode 100644 index 0000000000..9c8c3b42ba --- /dev/null +++ b/src/Http/Http.Features/src/IResponseCookies.cs @@ -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. + +namespace Microsoft.AspNetCore.Http +{ + /// + /// A wrapper for the response Set-Cookie header. + /// + public interface IResponseCookies + { + /// + /// Add a new cookie and value. + /// + /// Name of the new cookie. + /// Value of the new cookie. + void Append(string key, string value); + + /// + /// Add a new cookie. + /// + /// Name of the new cookie. + /// Value of the new cookie. + /// included in the new cookie setting. + void Append(string key, string value, CookieOptions options); + + /// + /// Sets an expired cookie. + /// + /// Name of the cookie to expire. + void Delete(string key); + + /// + /// Sets an expired cookie. + /// + /// Name of the cookie to expire. + /// + /// used to discriminate the particular cookie to expire. The + /// and values are especially important. + /// + void Delete(string key, CookieOptions options); + } +} diff --git a/src/Http/Http.Features/src/IResponseCookiesFeature.cs b/src/Http/Http.Features/src/IResponseCookiesFeature.cs new file mode 100644 index 0000000000..7ce1041840 --- /dev/null +++ b/src/Http/Http.Features/src/IResponseCookiesFeature.cs @@ -0,0 +1,16 @@ +// 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.Features +{ + /// + /// A helper for creating the response Set-Cookie header. + /// + public interface IResponseCookiesFeature + { + /// + /// Gets the wrapper for the response Set-Cookie header. + /// + IResponseCookies Cookies { get; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/IServiceProvidersFeature.cs b/src/Http/Http.Features/src/IServiceProvidersFeature.cs new file mode 100644 index 0000000000..aed0fc91de --- /dev/null +++ b/src/Http/Http.Features/src/IServiceProvidersFeature.cs @@ -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. + +using System; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface IServiceProvidersFeature + { + IServiceProvider RequestServices { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/ISession.cs b/src/Http/Http.Features/src/ISession.cs new file mode 100644 index 0000000000..6bd780684d --- /dev/null +++ b/src/Http/Http.Features/src/ISession.cs @@ -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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + public interface ISession + { + /// + /// Indicate whether the current session has loaded. + /// + bool IsAvailable { get; } + + /// + /// A unique identifier for the current session. This is not the same as the session cookie + /// since the cookie lifetime may not be the same as the session entry lifetime in the data store. + /// + string Id { get; } + + /// + /// Enumerates all the keys, if any. + /// + IEnumerable Keys { get; } + + /// + /// Load the session from the data store. This may throw if the data store is unavailable. + /// + /// + Task LoadAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Store the session in the data store. This may throw if the data store is unavailable. + /// + /// + Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Retrieve the value of the given key, if present. + /// + /// + /// + /// + bool TryGetValue(string key, out byte[] value); + + /// + /// Set the given key and value in the current session. This will throw if the session + /// was not established prior to sending the response. + /// + /// + /// + void Set(string key, byte[] value); + + /// + /// Remove the given key from the session if present. + /// + /// + void Remove(string key); + + /// + /// Remove all entries from the current session, if any. + /// The session cookie is not removed. + /// + void Clear(); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/ISessionFeature.cs b/src/Http/Http.Features/src/ISessionFeature.cs new file mode 100644 index 0000000000..2365299415 --- /dev/null +++ b/src/Http/Http.Features/src/ISessionFeature.cs @@ -0,0 +1,10 @@ +// 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.Features +{ + public interface ISessionFeature + { + ISession Session { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/ITlsConnectionFeature.cs b/src/Http/Http.Features/src/ITlsConnectionFeature.cs new file mode 100644 index 0000000000..c34a3339d5 --- /dev/null +++ b/src/Http/Http.Features/src/ITlsConnectionFeature.cs @@ -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.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public interface ITlsConnectionFeature + { + /// + /// Synchronously retrieves the client certificate, if any. + /// + X509Certificate2 ClientCertificate { get; set; } + + /// + /// Asynchronously retrieves the client certificate, if any. + /// + /// + Task GetClientCertificateAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs b/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs new file mode 100644 index 0000000000..d63333dd0a --- /dev/null +++ b/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs @@ -0,0 +1,35 @@ +// 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.Features +{ + /// + /// Provides information regarding TLS token binding parameters. + /// + /// + /// TLS token bindings help mitigate the risk of impersonation by an attacker in the + /// event an authenticated client's bearer tokens are somehow exfiltrated from the + /// client's machine. See https://datatracker.ietf.org/doc/draft-popov-token-binding/ + /// for more information. + /// + public interface ITlsTokenBindingFeature + { + /// + /// Gets the 'provided' token binding identifier associated with the request. + /// + /// The token binding identifier, or null if the client did not + /// supply a 'provided' token binding or valid proof of possession of the + /// associated private key. The caller should treat this identifier as an + /// opaque blob and should not try to parse it. + byte[] GetProvidedTokenBindingId(); + + /// + /// Gets the 'referred' token binding identifier associated with the request. + /// + /// The token binding identifier, or null if the client did not + /// supply a 'referred' token binding or valid proof of possession of the + /// associated private key. The caller should treat this identifier as an + /// opaque blob and should not try to parse it. + byte[] GetReferredTokenBindingId(); + } +} diff --git a/src/Http/Http.Features/src/ITrackingConsentFeature.cs b/src/Http/Http.Features/src/ITrackingConsentFeature.cs new file mode 100644 index 0000000000..e7fbeaeaf3 --- /dev/null +++ b/src/Http/Http.Features/src/ITrackingConsentFeature.cs @@ -0,0 +1,44 @@ +// 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.Features +{ + /// + /// Used to query, grant, and withdraw user consent regarding the storage of user + /// information related to site activity and functionality. + /// + public interface ITrackingConsentFeature + { + /// + /// Indicates if consent is required for the given request. + /// + bool IsConsentNeeded { get; } + + /// + /// Indicates if consent was given. + /// + bool HasConsent { get; } + + /// + /// Indicates either if consent has been given or if consent is not required. + /// + bool CanTrack { get; } + + /// + /// Grants consent for this request. If the response has not yet started then + /// this will also grant consent for future requests. + /// + void GrantConsent(); + + /// + /// Withdraws consent for this request. If the response has not yet started then + /// this will also withdraw consent for future requests. + /// + void WithdrawConsent(); + + /// + /// Creates a consent cookie for use when granting consent from a javascript client. + /// + string CreateConsentCookie(); + } +} diff --git a/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj b/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj new file mode 100644 index 0000000000..7a2310a6fd --- /dev/null +++ b/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core HTTP feature interface definitions. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + diff --git a/src/Http/Http.Features/src/SameSiteMode.cs b/src/Http/Http.Features/src/SameSiteMode.cs new file mode 100644 index 0000000000..0ae4481e3d --- /dev/null +++ b/src/Http/Http.Features/src/SameSiteMode.cs @@ -0,0 +1,14 @@ +// 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 +{ + // RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 + // This mirrors Microsoft.Net.Http.Headers.SameSiteMode + public enum SameSiteMode + { + None = 0, + Lax, + Strict + } +} diff --git a/src/Http/Http.Features/src/WebSocketAcceptContext.cs b/src/Http/Http.Features/src/WebSocketAcceptContext.cs new file mode 100644 index 0000000000..5e3659d647 --- /dev/null +++ b/src/Http/Http.Features/src/WebSocketAcceptContext.cs @@ -0,0 +1,10 @@ +// 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 +{ + public class WebSocketAcceptContext + { + public virtual string SubProtocol { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/src/baseline.netcore.json b/src/Http/Http.Features/src/baseline.netcore.json new file mode 100644 index 0000000000..6af2ceccf9 --- /dev/null +++ b/src/Http/Http.Features/src/baseline.netcore.json @@ -0,0 +1,2727 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Http.Features, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Domain", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Domain", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Expires", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Expires", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Secure", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Secure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SameSite", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.SameSiteMode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SameSite", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.SameSiteMode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsEssential", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsEssential", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Files", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormFileCollection", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IFormFile", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentDisposition", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Length", + "Parameters": [], + "ReturnType": "System.Int64", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileName", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OpenReadStream", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyTo", + "Parameters": [ + { + "Name": "target", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyToAsync", + "Parameters": [ + { + "Name": "target", + "Type": "System.IO.Stream" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IFormFileCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IReadOnlyList" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.IFormFile", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetFile", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.IFormFile", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetFiles", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.IReadOnlyList", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IDictionary" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IQueryCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IRequestCookieCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.IResponseCookies", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Delete", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Delete", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.ISession", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsAvailable", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Id", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "LoadAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CommitAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Byte[]", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.SameSiteMode", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "None", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Lax", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "Strict", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.WebSocketAcceptContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SubProtocol", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SubProtocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FeatureCollection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Revision", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + } + ], + "ReturnType": "System.Object", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get", + "Parameters": [], + "ReturnType": "T0", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "instance", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "defaults", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FeatureReference", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Fetch", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Update", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + }, + { + "Name": "feature", + "Type": "T0" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Default", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.FeatureReference", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "T", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FeatureReferences", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Collection", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Revision", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fetch", + "Parameters": [ + { + "Name": "cached", + "Type": "T0", + "Direction": "Ref" + }, + { + "Name": "state", + "Type": "T1" + }, + { + "Name": "factory", + "Type": "System.Func" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "Class": true, + "BaseTypeOrInterfaces": [] + }, + { + "ParameterName": "TState", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Fetch", + "Parameters": [ + { + "Name": "cached", + "Type": "T0", + "Direction": "Ref" + }, + { + "Name": "factory", + "Type": "System.Func" + } + ], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "Class": true, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Cache", + "Parameters": [], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TCache", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Revision", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + } + ], + "ReturnType": "System.Object", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get", + "Parameters": [], + "ReturnType": "T0", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "instance", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HasFormContentType", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Form", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Form", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IFormCollection" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadForm", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AllowSynchronousIO", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AllowSynchronousIO", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DisableRequestBuffering", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DisableResponseBuffering", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ConnectionId", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConnectionId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemotePort", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemotePort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalPort", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalPort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxRequestBodySize", + "Parameters": [], + "ReturnType": "System.Nullable", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxRequestBodySize", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Protocol", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Protocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Scheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Method", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Method", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PathBase", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PathBase", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_QueryString", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_QueryString", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RawTarget", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RawTarget", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_TraceIdentifier", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TraceIdentifier", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestAborted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestAborted", + "Parameters": [ + { + "Name": "value", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_StatusCode", + "Parameters": [], + "ReturnType": "System.Int32", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StatusCode", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReasonPhrase", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReasonPhrase", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasStarted", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnCompleted", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "SendFileAsync", + "Parameters": [ + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "count", + "Type": "System.Nullable" + }, + { + "Name": "cancellation", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpUpgradeFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsUpgradableRequest", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UpgradeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsWebSocketRequest", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AcceptAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.WebSocketAcceptContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IItemsFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Items", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IQueryFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Query", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IQueryCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Query", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IQueryCollection" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IRequestCookiesFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IRequestCookieCollection", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookies", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IRequestCookieCollection" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IResponseCookies", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ISessionFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Session", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ISession", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Session", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.ISession" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ClientCertificate", + "Parameters": [], + "ReturnType": "System.Security.Cryptography.X509Certificates.X509Certificate2", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientCertificate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetClientCertificateAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ITlsTokenBindingFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetProvidedTokenBindingId", + "Parameters": [], + "ReturnType": "System.Byte[]", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetReferredTokenBindingId", + "Parameters": [], + "ReturnType": "System.Byte[]", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ITrackingConsentFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_IsConsentNeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasConsent", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanTrack", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GrantConsent", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WithdrawConsent", + "Parameters": [], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateConsentCookie", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.AuthenticateContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accepted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Description", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Error", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Authenticated", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "description", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "NotAuthenticated", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [ + { + "Name": "error", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeBehavior", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Automatic", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Unauthorized", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "Forbidden", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Behavior", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeBehavior", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accepted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "behavior", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeBehavior" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.DescribeSchemesContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Results", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [ + { + "Name": "description", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetDescriptions", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.DescribeSchemesContext" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.AuthenticateContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.ChallengeContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.SignInContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.SignOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Handler", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.SignInContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accepted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.SignOutContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Accepted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Http.Features/test/Authentication/AuthenticateContextTest.cs b/src/Http/Http.Features/test/Authentication/AuthenticateContextTest.cs new file mode 100644 index 0000000000..c4d901322e --- /dev/null +++ b/src/Http/Http.Features/test/Authentication/AuthenticateContextTest.cs @@ -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 System.Security.Claims; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class AuthenticateContextTest + { + [Fact] + public void AuthenticateContext_Authenticated() + { + // Arrange + var context = new AuthenticateContext("test"); + + var principal = new ClaimsPrincipal(); + var properties = new Dictionary(); + var description = new Dictionary(); + + // Act + context.Authenticated(principal, properties, description); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Same(description, context.Description); + Assert.Null(context.Error); + Assert.Same(principal, context.Principal); + Assert.Same(properties, context.Properties); + } + + [Fact] + public void AuthenticateContext_Authenticated_SetsUnusedPropertiesToDefault() + { + // Arrange + var context = new AuthenticateContext("test"); + + var principal = new ClaimsPrincipal(); + var properties = new Dictionary(); + var description = new Dictionary(); + + context.Failed(new Exception()); + + // Act + context.Authenticated(principal, properties, description); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Same(description, context.Description); + Assert.Null(context.Error); + Assert.Same(principal, context.Principal); + Assert.Same(properties, context.Properties); + } + + [Fact] + public void AuthenticateContext_Failed() + { + // Arrange + var context = new AuthenticateContext("test"); + + var exception = new Exception(); + + // Act + context.Failed(exception); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Same(exception, context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + + [Fact] + public void AuthenticateContext_Failed_SetsUnusedPropertiesToDefault() + { + // Arrange + var context = new AuthenticateContext("test"); + + var exception = new Exception(); + + context.Authenticated(new ClaimsPrincipal(), new Dictionary(), new Dictionary()); + + // Act + context.Failed(exception); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Same(exception, context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + + [Fact] + public void AuthenticateContext_NotAuthenticated() + { + // Arrange + var context = new AuthenticateContext("test"); + + // Act + context.NotAuthenticated(); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Null(context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + + [Fact] + public void AuthenticateContext_NotAuthenticated_SetsUnusedPropertiesToDefault_Authenticated() + { + // Arrange + var context = new AuthenticateContext("test"); + + var exception = new Exception(); + + context.Authenticated(new ClaimsPrincipal(), new Dictionary(), new Dictionary()); + + // Act + context.NotAuthenticated(); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Null(context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + + [Fact] + public void AuthenticateContext_NotAuthenticated_SetsUnusedPropertiesToDefault_Failed() + { + // Arrange + var context = new AuthenticateContext("test"); + + context.Failed(new Exception()); + + context.NotAuthenticated(); + + // Assert + Assert.True(context.Accepted); + Assert.Equal("test", context.AuthenticationScheme); + Assert.Null(context.Description); + Assert.Null(context.Error); + Assert.Null(context.Principal); + Assert.Null(context.Properties); + } + } +} diff --git a/src/Http/Http.Features/test/FeatureCollectionTests.cs b/src/Http/Http.Features/test/FeatureCollectionTests.cs new file mode 100644 index 0000000000..36ad77f678 --- /dev/null +++ b/src/Http/Http.Features/test/FeatureCollectionTests.cs @@ -0,0 +1,48 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FeatureCollectionTests + { + [Fact] + public void AddedInterfaceIsReturned() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + + interfaces[typeof(IThing)] = thing; + + object thing2 = interfaces[typeof(IThing)]; + Assert.Equal(thing2, thing); + } + + [Fact] + public void IndexerAlsoAddsItems() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + + interfaces[typeof(IThing)] = thing; + + Assert.Equal(interfaces[typeof(IThing)], thing); + } + + [Fact] + public void SetNullValueRemoves() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + + interfaces[typeof(IThing)] = thing; + Assert.Equal(interfaces[typeof(IThing)], thing); + + interfaces[typeof(IThing)] = null; + + object thing2 = interfaces[typeof(IThing)]; + Assert.Null(thing2); + } + } +} diff --git a/src/Http/Http.Features/test/IThing.cs b/src/Http/Http.Features/test/IThing.cs new file mode 100644 index 0000000000..f5b0a1e122 --- /dev/null +++ b/src/Http/Http.Features/test/IThing.cs @@ -0,0 +1,10 @@ +// 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.Features +{ + public interface IThing + { + string Hello(); + } +} \ No newline at end of file diff --git a/src/Http/Http.Features/test/Microsoft.AspNetCore.Http.Features.Tests.csproj b/src/Http/Http.Features/test/Microsoft.AspNetCore.Http.Features.Tests.csproj new file mode 100644 index 0000000000..b7c77fc19f --- /dev/null +++ b/src/Http/Http.Features/test/Microsoft.AspNetCore.Http.Features.Tests.csproj @@ -0,0 +1,11 @@ + + + + $(StandardTestTfms) + + + + + + + diff --git a/src/Http/Http.Features/test/Thing.cs b/src/Http/Http.Features/test/Thing.cs new file mode 100644 index 0000000000..27a2c0e285 --- /dev/null +++ b/src/Http/Http.Features/test/Thing.cs @@ -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.AspNetCore.Http.Features +{ + public class Thing : IThing + { + public string Hello() + { + return "World"; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Authentication/DefaultAuthenticationManager.cs b/src/Http/Http/src/Authentication/DefaultAuthenticationManager.cs new file mode 100644 index 0000000000..9f4121f4cb --- /dev/null +++ b/src/Http/Http/src/Authentication/DefaultAuthenticationManager.cs @@ -0,0 +1,184 @@ +// 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.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Http.Authentication.Internal +{ + [Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")] + public class DefaultAuthenticationManager : AuthenticationManager + { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _newAuthenticationFeature = f => new HttpAuthenticationFeature(); + + private HttpContext _context; + private FeatureReferences _features; + + public DefaultAuthenticationManager(HttpContext context) + { + Initialize(context); + } + + public virtual void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences(context.Features); + } + + public virtual void Uninitialize() + { + _features = default(FeatureReferences); + } + + public override HttpContext HttpContext => _context; + + private IHttpAuthenticationFeature HttpAuthenticationFeature => + _features.Fetch(ref _features.Cache, _newAuthenticationFeature); + + public override IEnumerable GetAuthenticationSchemes() + { +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + if (handler == null) + { + return new AuthenticationDescription[0]; + } + + var describeContext = new DescribeSchemesContext(); + handler.GetDescriptions(describeContext); + return describeContext.Results.Select(description => new AuthenticationDescription(description)); + } + + // Remove once callers have been switched to GetAuthenticateInfoAsync + public override async Task AuthenticateAsync(AuthenticateContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + if (handler != null) + { + await handler.AuthenticateAsync(context); + } + + if (!context.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {context.AuthenticationScheme}"); + } + } + + public override async Task GetAuthenticateInfoAsync(string authenticationScheme) + { + if (authenticationScheme == null) + { + throw new ArgumentNullException(nameof(authenticationScheme)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + var context = new AuthenticateContext(authenticationScheme); + if (handler != null) + { + await handler.AuthenticateAsync(context); + } + + if (!context.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {context.AuthenticationScheme}"); + } + + return new AuthenticateInfo + { + Principal = context.Principal, + Properties = new AuthenticationProperties(context.Properties), + Description = new AuthenticationDescription(context.Description) + }; + } + + public override async Task ChallengeAsync(string authenticationScheme, AuthenticationProperties properties, ChallengeBehavior behavior) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + + var challengeContext = new ChallengeContext(authenticationScheme, properties?.Items, behavior); + if (handler != null) + { + await handler.ChallengeAsync(challengeContext); + } + + if (!challengeContext.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {authenticationScheme}"); + } + } + + public override async Task SignInAsync(string authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + + var signInContext = new SignInContext(authenticationScheme, principal, properties?.Items); + if (handler != null) + { + await handler.SignInAsync(signInContext); + } + + if (!signInContext.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {authenticationScheme}"); + } + } + + public override async Task SignOutAsync(string authenticationScheme, AuthenticationProperties properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var handler = HttpAuthenticationFeature.Handler; +#pragma warning restore CS0618 // Type or member is obsolete + + var signOutContext = new SignOutContext(authenticationScheme, properties?.Items); + if (handler != null) + { + await handler.SignOutAsync(signOutContext); + } + + if (!signOutContext.Accepted) + { + throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {authenticationScheme}"); + } + } + } +} diff --git a/src/Http/Http/src/DefaultHttpContext.cs b/src/Http/Http/src/DefaultHttpContext.cs new file mode 100644 index 0000000000..d02ad6322b --- /dev/null +++ b/src/Http/Http/src/DefaultHttpContext.cs @@ -0,0 +1,223 @@ +// 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; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Http.Authentication.Internal; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.Http.Internal; + +namespace Microsoft.AspNetCore.Http +{ + public class DefaultHttpContext : HttpContext + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _newItemsFeature = f => new ItemsFeature(); + private readonly static Func _newServiceProvidersFeature = f => new ServiceProvidersFeature(); + private readonly static Func _newHttpAuthenticationFeature = f => new HttpAuthenticationFeature(); + private readonly static Func _newHttpRequestLifetimeFeature = f => new HttpRequestLifetimeFeature(); + private readonly static Func _newSessionFeature = f => new DefaultSessionFeature(); + private readonly static Func _nullSessionFeature = f => null; + private readonly static Func _newHttpRequestIdentifierFeature = f => new HttpRequestIdentifierFeature(); + + private FeatureReferences _features; + + private HttpRequest _request; + private HttpResponse _response; + +#pragma warning disable CS0618 // Type or member is obsolete + private AuthenticationManager _authenticationManager; +#pragma warning restore CS0618 // Type or member is obsolete + + private ConnectionInfo _connection; + private WebSocketManager _websockets; + + public DefaultHttpContext() + : this(new FeatureCollection()) + { + Features.Set(new HttpRequestFeature()); + Features.Set(new HttpResponseFeature()); + } + + public DefaultHttpContext(IFeatureCollection features) + { + Initialize(features); + } + + public virtual void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences(features); + _request = InitializeHttpRequest(); + _response = InitializeHttpResponse(); + } + + public virtual void Uninitialize() + { + _features = default(FeatureReferences); + if (_request != null) + { + UninitializeHttpRequest(_request); + _request = null; + } + if (_response != null) + { + UninitializeHttpResponse(_response); + _response = null; + } + if (_authenticationManager != null) + { +#pragma warning disable CS0618 // Type or member is obsolete + UninitializeAuthenticationManager(_authenticationManager); +#pragma warning restore CS0618 // Type or member is obsolete + _authenticationManager = null; + } + if (_connection != null) + { + UninitializeConnectionInfo(_connection); + _connection = null; + } + if (_websockets != null) + { + UninitializeWebSocketManager(_websockets); + _websockets = null; + } + } + + private IItemsFeature ItemsFeature => + _features.Fetch(ref _features.Cache.Items, _newItemsFeature); + + private IServiceProvidersFeature ServiceProvidersFeature => + _features.Fetch(ref _features.Cache.ServiceProviders, _newServiceProvidersFeature); + + private IHttpAuthenticationFeature HttpAuthenticationFeature => + _features.Fetch(ref _features.Cache.Authentication, _newHttpAuthenticationFeature); + + private IHttpRequestLifetimeFeature LifetimeFeature => + _features.Fetch(ref _features.Cache.Lifetime, _newHttpRequestLifetimeFeature); + + private ISessionFeature SessionFeature => + _features.Fetch(ref _features.Cache.Session, _newSessionFeature); + + private ISessionFeature SessionFeatureOrNull => + _features.Fetch(ref _features.Cache.Session, _nullSessionFeature); + + + private IHttpRequestIdentifierFeature RequestIdentifierFeature => + _features.Fetch(ref _features.Cache.RequestIdentifier, _newHttpRequestIdentifierFeature); + + public override IFeatureCollection Features => _features.Collection; + + public override HttpRequest Request => _request; + + public override HttpResponse Response => _response; + + public override ConnectionInfo Connection => _connection ?? (_connection = InitializeConnectionInfo()); + + /// + /// This is obsolete and will be removed in a future version. + /// The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. + /// See https://go.microsoft.com/fwlink/?linkid=845470. + /// + [Obsolete("This is obsolete and will be removed in a future version. The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. See https://go.microsoft.com/fwlink/?linkid=845470.")] + public override AuthenticationManager Authentication => _authenticationManager ?? (_authenticationManager = InitializeAuthenticationManager()); + + public override WebSocketManager WebSockets => _websockets ?? (_websockets = InitializeWebSocketManager()); + + + public override ClaimsPrincipal User + { + get + { + var user = HttpAuthenticationFeature.User; + if (user == null) + { + user = new ClaimsPrincipal(new ClaimsIdentity()); + HttpAuthenticationFeature.User = user; + } + return user; + } + set { HttpAuthenticationFeature.User = value; } + } + + public override IDictionary Items + { + get { return ItemsFeature.Items; } + set { ItemsFeature.Items = value; } + } + + public override IServiceProvider RequestServices + { + get { return ServiceProvidersFeature.RequestServices; } + set { ServiceProvidersFeature.RequestServices = value; } + } + + public override CancellationToken RequestAborted + { + get { return LifetimeFeature.RequestAborted; } + set { LifetimeFeature.RequestAborted = value; } + } + + public override string TraceIdentifier + { + get { return RequestIdentifierFeature.TraceIdentifier; } + set { RequestIdentifierFeature.TraceIdentifier = value; } + } + + public override ISession Session + { + get + { + var feature = SessionFeatureOrNull; + if (feature == null) + { + throw new InvalidOperationException("Session has not been configured for this application " + + "or request."); + } + return feature.Session; + } + set + { + SessionFeature.Session = value; + } + } + + + + public override void Abort() + { + LifetimeFeature.Abort(); + } + + + protected virtual HttpRequest InitializeHttpRequest() => new DefaultHttpRequest(this); + protected virtual void UninitializeHttpRequest(HttpRequest instance) { } + + protected virtual HttpResponse InitializeHttpResponse() => new DefaultHttpResponse(this); + protected virtual void UninitializeHttpResponse(HttpResponse instance) { } + + protected virtual ConnectionInfo InitializeConnectionInfo() => new DefaultConnectionInfo(Features); + protected virtual void UninitializeConnectionInfo(ConnectionInfo instance) { } + + [Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")] + protected virtual AuthenticationManager InitializeAuthenticationManager() => new DefaultAuthenticationManager(this); + [Obsolete("This is obsolete and will be removed in a future version. See https://go.microsoft.com/fwlink/?linkid=845470.")] + protected virtual void UninitializeAuthenticationManager(AuthenticationManager instance) { } + + protected virtual WebSocketManager InitializeWebSocketManager() => new DefaultWebSocketManager(Features); + protected virtual void UninitializeWebSocketManager(WebSocketManager instance) { } + + struct FeatureInterfaces + { + public IItemsFeature Items; + public IServiceProvidersFeature ServiceProviders; + public IHttpAuthenticationFeature Authentication; + public IHttpRequestLifetimeFeature Lifetime; + public ISessionFeature Session; + public IHttpRequestIdentifierFeature RequestIdentifier; + } + } +} diff --git a/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs b/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs new file mode 100644 index 0000000000..557ee42155 --- /dev/null +++ b/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs @@ -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 Microsoft.AspNetCore.Http.Internal; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Extension methods for enabling buffering in an . + /// + public static class HttpRequestRewindExtensions + { + /// + /// Ensure the can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than 30K bytes to disk. + /// + /// The to prepare. + /// + /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// + public static void EnableBuffering(this HttpRequest request) + { + BufferingHelper.EnableRewind(request); + } + + /// + /// Ensure the can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than bytes to + /// disk. + /// + /// The to prepare. + /// + /// The maximum size in bytes of the in-memory used to buffer the + /// stream. Larger request bodies are written to disk. + /// + /// + /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// + public static void EnableBuffering(this HttpRequest request, int bufferThreshold) + { + BufferingHelper.EnableRewind(request, bufferThreshold); + } + + /// + /// Ensure the can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than 30K bytes to disk. + /// + /// The to prepare. + /// + /// The maximum size in bytes of the request body. An attempt to read beyond this limit will cause an + /// . + /// + /// + /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// + public static void EnableBuffering(this HttpRequest request, long bufferLimit) + { + BufferingHelper.EnableRewind(request, bufferLimit: bufferLimit); + } + + /// + /// Ensure the can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than bytes to + /// disk. + /// + /// The to prepare. + /// + /// The maximum size in bytes of the in-memory used to buffer the + /// stream. Larger request bodies are written to disk. + /// + /// + /// The maximum size in bytes of the request body. An attempt to read beyond this limit will cause an + /// . + /// + /// + /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// + public static void EnableBuffering(this HttpRequest request, int bufferThreshold, long bufferLimit) + { + BufferingHelper.EnableRewind(request, bufferThreshold, bufferLimit); + } + } +} diff --git a/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs b/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs new file mode 100644 index 0000000000..9a14b65712 --- /dev/null +++ b/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs @@ -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; + +namespace Microsoft.AspNetCore.Http.Features.Authentication +{ + public class HttpAuthenticationFeature : IHttpAuthenticationFeature + { + public ClaimsPrincipal User + { + get; + set; + } + + public IAuthenticationHandler Handler + { + get; + set; + } + } +} diff --git a/src/Http/Http/src/Features/DefaultSessionFeature.cs b/src/Http/Http/src/Features/DefaultSessionFeature.cs new file mode 100644 index 0000000000..6790133467 --- /dev/null +++ b/src/Http/Http/src/Features/DefaultSessionFeature.cs @@ -0,0 +1,14 @@ +// 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.Features +{ + /// + /// This type exists only for the purpose of unit testing where the user can directly set the + /// property without the need for creating a . + /// + public class DefaultSessionFeature : ISessionFeature + { + public ISession Session { get; set; } + } +} diff --git a/src/Http/Http/src/Features/FormFeature.cs b/src/Http/Http/src/Features/FormFeature.cs new file mode 100644 index 0000000000..f091e3b166 --- /dev/null +++ b/src/Http/Http/src/Features/FormFeature.cs @@ -0,0 +1,323 @@ +// 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.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FormFeature : IFormFeature + { + private static readonly FormOptions DefaultFormOptions = new FormOptions(); + + private readonly HttpRequest _request; + private readonly FormOptions _options; + private Task _parsedFormTask; + private IFormCollection _form; + + public FormFeature(IFormCollection form) + { + if (form == null) + { + throw new ArgumentNullException(nameof(form)); + } + + Form = form; + } + public FormFeature(HttpRequest request) + : this(request, DefaultFormOptions) + { + } + + public FormFeature(HttpRequest request, FormOptions options) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _request = request; + _options = options; + } + + private MediaTypeHeaderValue ContentType + { + get + { + MediaTypeHeaderValue mt; + MediaTypeHeaderValue.TryParse(_request.ContentType, out mt); + return mt; + } + } + + public bool HasFormContentType + { + get + { + // Set directly + if (Form != null) + { + return true; + } + + var contentType = ContentType; + return HasApplicationFormContentType(contentType) || HasMultipartFormContentType(contentType); + } + } + + public IFormCollection Form + { + get { return _form; } + set + { + _parsedFormTask = null; + _form = value; + } + } + + public IFormCollection ReadForm() + { + if (Form != null) + { + return Form; + } + + if (!HasFormContentType) + { + throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); + } + + // TODO: Issue #456 Avoid Sync-over-Async http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx + // TODO: How do we prevent thread exhaustion? + return ReadFormAsync().GetAwaiter().GetResult(); + } + + public Task ReadFormAsync() => ReadFormAsync(CancellationToken.None); + + public Task ReadFormAsync(CancellationToken cancellationToken) + { + // Avoid state machine and task allocation for repeated reads + if (_parsedFormTask == null) + { + if (Form != null) + { + _parsedFormTask = Task.FromResult(Form); + } + else + { + _parsedFormTask = InnerReadFormAsync(cancellationToken); + } + } + return _parsedFormTask; + } + + private async Task InnerReadFormAsync(CancellationToken cancellationToken) + { + if (!HasFormContentType) + { + throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (_options.BufferBody) + { + _request.EnableRewind(_options.MemoryBufferThreshold, _options.BufferBodyLengthLimit); + } + + FormCollection formFields = null; + FormFileCollection files = null; + + // Some of these code paths use StreamReader which does not support cancellation tokens. + using (cancellationToken.Register((state) => ((HttpContext)state).Abort(), _request.HttpContext)) + { + var contentType = ContentType; + // Check the content-type + if (HasApplicationFormContentType(contentType)) + { + var encoding = FilterEncoding(contentType.Encoding); + using (var formReader = new FormReader(_request.Body, encoding) + { + ValueCountLimit = _options.ValueCountLimit, + KeyLengthLimit = _options.KeyLengthLimit, + ValueLengthLimit = _options.ValueLengthLimit, + }) + { + formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken)); + } + } + else if (HasMultipartFormContentType(contentType)) + { + var formAccumulator = new KeyValueAccumulator(); + + var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit); + var multipartReader = new MultipartReader(boundary, _request.Body) + { + HeadersCountLimit = _options.MultipartHeadersCountLimit, + HeadersLengthLimit = _options.MultipartHeadersLengthLimit, + BodyLengthLimit = _options.MultipartBodyLengthLimit, + }; + var section = await multipartReader.ReadNextSectionAsync(cancellationToken); + while (section != null) + { + // Parse the content disposition here and pass it further to avoid reparsings + ContentDispositionHeaderValue contentDisposition; + ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition); + + if (contentDisposition.IsFileDisposition()) + { + var fileSection = new FileMultipartSection(section, contentDisposition); + + // Enable buffering for the file if not already done for the full body + section.EnableRewind( + _request.HttpContext.Response.RegisterForDispose, + _options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit); + + // Find the end + await section.Body.DrainAsync(cancellationToken); + + var name = fileSection.Name; + var fileName = fileSection.FileName; + + FormFile file; + if (section.BaseStreamOffset.HasValue) + { + // Relative reference to buffered request body + file = new FormFile(_request.Body, section.BaseStreamOffset.Value, section.Body.Length, name, fileName); + } + else + { + // Individually buffered file body + file = new FormFile(section.Body, 0, section.Body.Length, name, fileName); + } + file.Headers = new HeaderDictionary(section.Headers); + + if (files == null) + { + files = new FormFileCollection(); + } + if (files.Count >= _options.ValueCountLimit) + { + throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded."); + } + files.Add(file); + } + else if (contentDisposition.IsFormDisposition()) + { + var formDataSection = new FormMultipartSection(section, contentDisposition); + + // Content-Disposition: form-data; name="key" + // + // value + + // Do not limit the key name length here because the mulipart headers length limit is already in effect. + var key = formDataSection.Name; + var value = await formDataSection.GetValueAsync(); + + formAccumulator.Append(key, value); + if (formAccumulator.ValueCount > _options.ValueCountLimit) + { + throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded."); + } + } + else + { + System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + section.ContentDisposition); + } + + section = await multipartReader.ReadNextSectionAsync(cancellationToken); + } + + if (formAccumulator.HasValues) + { + formFields = new FormCollection(formAccumulator.GetResults(), files); + } + } + } + + // Rewind so later readers don't have to. + if (_request.Body.CanSeek) + { + _request.Body.Seek(0, SeekOrigin.Begin); + } + + if (formFields != null) + { + Form = formFields; + } + else if (files != null) + { + Form = new FormCollection(null, files); + } + else + { + Form = FormCollection.Empty; + } + + return Form; + } + + private Encoding FilterEncoding(Encoding encoding) + { + // UTF-7 is insecure and should not be honored. UTF-8 will succeed for most cases. + if (encoding == null || Encoding.UTF7.Equals(encoding)) + { + return Encoding.UTF8; + } + return encoding; + } + + private bool HasApplicationFormContentType(MediaTypeHeaderValue contentType) + { + // Content-Type: application/x-www-form-urlencoded; charset=utf-8 + return contentType != null && contentType.MediaType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase); + } + + private bool HasMultipartFormContentType(MediaTypeHeaderValue contentType) + { + // Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq + return contentType != null && contentType.MediaType.Equals("multipart/form-data", StringComparison.OrdinalIgnoreCase); + } + + private bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + // Content-Disposition: form-data; name="key"; + return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data") + && StringSegment.IsNullOrEmpty(contentDisposition.FileName) && StringSegment.IsNullOrEmpty(contentDisposition.FileNameStar); + } + + private bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" + return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data") + && (!StringSegment.IsNullOrEmpty(contentDisposition.FileName) || !StringSegment.IsNullOrEmpty(contentDisposition.FileNameStar)); + } + + // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" + // The spec says 70 characters is a reasonable limit. + private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) + { + var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary); + if (StringSegment.IsNullOrEmpty(boundary)) + { + throw new InvalidDataException("Missing content-type boundary."); + } + if (boundary.Length > lengthLimit) + { + throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded."); + } + return boundary.ToString(); + } + } +} diff --git a/src/Http/Http/src/Features/FormOptions.cs b/src/Http/Http/src/Features/FormOptions.cs new file mode 100644 index 0000000000..17e521b215 --- /dev/null +++ b/src/Http/Http/src/Features/FormOptions.cs @@ -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.IO; +using Microsoft.AspNetCore.WebUtilities; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FormOptions + { + public const int DefaultMemoryBufferThreshold = 1024 * 64; + public const int DefaultBufferBodyLengthLimit = 1024 * 1024 * 128; + public const int DefaultMultipartBoundaryLengthLimit = 128; + public const long DefaultMultipartBodyLengthLimit = 1024 * 1024 * 128; + + /// + /// Enables full request body buffering. Use this if multiple components need to read the raw stream. + /// The default value is false. + /// + public bool BufferBody { get; set; } = false; + + /// + /// If is enabled, this many bytes of the body will be buffered in memory. + /// If this threshold is exceeded then the buffer will be moved to a temp file on disk instead. + /// This also applies when buffering individual multipart section bodies. + /// + public int MemoryBufferThreshold { get; set; } = DefaultMemoryBufferThreshold; + + /// + /// If is enabled, this is the limit for the total number of bytes that will + /// be buffered. Forms that exceed this limit will throw an when parsed. + /// + public long BufferBodyLengthLimit { get; set; } = DefaultBufferBodyLengthLimit; + + /// + /// A limit for the number of form entries to allow. + /// Forms that exceed this limit will throw an when parsed. + /// + public int ValueCountLimit { get; set; } = FormReader.DefaultValueCountLimit; + + /// + /// A limit on the length of individual keys. Forms containing keys that exceed this limit will + /// throw an when parsed. + /// + public int KeyLengthLimit { get; set; } = FormReader.DefaultKeyLengthLimit; + + /// + /// A limit on the length of individual form values. Forms containing values that exceed this + /// limit will throw an when parsed. + /// + public int ValueLengthLimit { get; set; } = FormReader.DefaultValueLengthLimit; + + /// + /// A limit for the length of the boundary identifier. Forms with boundaries that exceed this + /// limit will throw an when parsed. + /// + public int MultipartBoundaryLengthLimit { get; set; } = DefaultMultipartBoundaryLengthLimit; + + /// + /// A limit for the number of headers to allow in each multipart section. Headers with the same name will + /// be combined. Form sections that exceed this limit will throw an + /// when parsed. + /// + public int MultipartHeadersCountLimit { get; set; } = MultipartReader.DefaultHeadersCountLimit; + + /// + /// A limit for the total length of the header keys and values in each multipart section. + /// Form sections that exceed this limit will throw an when parsed. + /// + public int MultipartHeadersLengthLimit { get; set; } = MultipartReader.DefaultHeadersLengthLimit; + + /// + /// A limit for the length of each multipart body. Forms sections that exceed this limit will throw an + /// when parsed. + /// + public long MultipartBodyLengthLimit { get; set; } = DefaultMultipartBodyLengthLimit; + } +} diff --git a/src/Http/Http/src/Features/HttpConnectionFeature.cs b/src/Http/Http/src/Features/HttpConnectionFeature.cs new file mode 100644 index 0000000000..2e8d5b0a1c --- /dev/null +++ b/src/Http/Http/src/Features/HttpConnectionFeature.cs @@ -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.Net; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpConnectionFeature : IHttpConnectionFeature + { + public string ConnectionId { get; set; } + + public IPAddress LocalIpAddress { get; set; } + + public int LocalPort { get; set; } + + public IPAddress RemoteIpAddress { get; set; } + + public int RemotePort { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/HttpRequestFeature.cs b/src/Http/Http/src/Features/HttpRequestFeature.cs new file mode 100644 index 0000000000..b8b667bf4e --- /dev/null +++ b/src/Http/Http/src/Features/HttpRequestFeature.cs @@ -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.IO; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpRequestFeature : IHttpRequestFeature + { + public HttpRequestFeature() + { + Headers = new HeaderDictionary(); + Body = Stream.Null; + Protocol = string.Empty; + Scheme = string.Empty; + Method = string.Empty; + PathBase = string.Empty; + Path = string.Empty; + QueryString = string.Empty; + RawTarget = string.Empty; + } + + public string Protocol { get; set; } + public string Scheme { get; set; } + public string Method { get; set; } + public string PathBase { get; set; } + public string Path { get; set; } + public string QueryString { get; set; } + public string RawTarget { get; set; } + public IHeaderDictionary Headers { get; set; } + public Stream Body { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs b/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs new file mode 100644 index 0000000000..34663937a5 --- /dev/null +++ b/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs @@ -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; +using System.Threading; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpRequestIdentifierFeature : IHttpRequestIdentifierFeature + { + // Base32 encoding - in ascii sort order for easy text based sorting + private static readonly string _encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + // Seed the _requestId for this application instance with + // the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001 + // for a roughly increasing _requestId over restarts + private static long _requestId = DateTime.UtcNow.Ticks; + + private string _id = null; + + public string TraceIdentifier + { + get + { + // Don't incur the cost of generating the request ID until it's asked for + if (_id == null) + { + _id = GenerateRequestId(Interlocked.Increment(ref _requestId)); + } + return _id; + } + set + { + _id = value; + } + } + + private static unsafe string GenerateRequestId(long id) + { + // The following routine is ~310% faster than calling long.ToString() on x64 + // and ~600% faster than calling long.ToString() on x86 in tight loops of 1 million+ iterations + // See: https://github.com/aspnet/Hosting/pull/385 + + // stackalloc to allocate array on stack rather than heap + char* charBuffer = stackalloc char[13]; + + charBuffer[0] = _encode32Chars[(int)(id >> 60) & 31]; + charBuffer[1] = _encode32Chars[(int)(id >> 55) & 31]; + charBuffer[2] = _encode32Chars[(int)(id >> 50) & 31]; + charBuffer[3] = _encode32Chars[(int)(id >> 45) & 31]; + charBuffer[4] = _encode32Chars[(int)(id >> 40) & 31]; + charBuffer[5] = _encode32Chars[(int)(id >> 35) & 31]; + charBuffer[6] = _encode32Chars[(int)(id >> 30) & 31]; + charBuffer[7] = _encode32Chars[(int)(id >> 25) & 31]; + charBuffer[8] = _encode32Chars[(int)(id >> 20) & 31]; + charBuffer[9] = _encode32Chars[(int)(id >> 15) & 31]; + charBuffer[10] = _encode32Chars[(int)(id >> 10) & 31]; + charBuffer[11] = _encode32Chars[(int)(id >> 5) & 31]; + charBuffer[12] = _encode32Chars[(int)id & 31]; + + // string ctor overload that takes char* + return new string(charBuffer, 0, 13); + } + } +} diff --git a/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs b/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs new file mode 100644 index 0000000000..df327d0758 --- /dev/null +++ b/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpRequestLifetimeFeature : IHttpRequestLifetimeFeature + { + public CancellationToken RequestAborted { get; set; } + + public void Abort() + { + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/HttpResponseFeature.cs b/src/Http/Http/src/Features/HttpResponseFeature.cs new file mode 100644 index 0000000000..a02a79088b --- /dev/null +++ b/src/Http/Http/src/Features/HttpResponseFeature.cs @@ -0,0 +1,40 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpResponseFeature : IHttpResponseFeature + { + public HttpResponseFeature() + { + StatusCode = 200; + Headers = new HeaderDictionary(); + Body = Stream.Null; + } + + public int StatusCode { get; set; } + + public string ReasonPhrase { get; set; } + + public IHeaderDictionary Headers { get; set; } + + public Stream Body { get; set; } + + public virtual bool HasStarted + { + get { return false; } + } + + public virtual void OnStarting(Func callback, object state) + { + } + + public virtual void OnCompleted(Func callback, object state) + { + } + } +} diff --git a/src/Http/Http/src/Features/ItemsFeature.cs b/src/Http/Http/src/Features/ItemsFeature.cs new file mode 100644 index 0000000000..6bf0669b45 --- /dev/null +++ b/src/Http/Http/src/Features/ItemsFeature.cs @@ -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. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http.Internal; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class ItemsFeature : IItemsFeature + { + public ItemsFeature() + { + Items = new ItemsDictionary(); + } + + public IDictionary Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/QueryFeature.cs b/src/Http/Http/src/Features/QueryFeature.cs new file mode 100644 index 0000000000..36781ef16e --- /dev/null +++ b/src/Http/Http/src/Features/QueryFeature.cs @@ -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 Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.WebUtilities; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class QueryFeature : IQueryFeature + { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullRequestFeature = f => null; + + private FeatureReferences _features; + + private string _original; + private IQueryCollection _parsedValues; + + public QueryFeature(IQueryCollection query) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + _parsedValues = query; + } + + public QueryFeature(IFeatureCollection features) + { + if (features == null) + { + throw new ArgumentNullException(nameof(features)); + } + + _features = new FeatureReferences(features); + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache, _nullRequestFeature); + + public IQueryCollection Query + { + get + { + if (_features.Collection == null) + { + if (_parsedValues == null) + { + _parsedValues = QueryCollection.Empty; + } + return _parsedValues; + } + + var current = HttpRequestFeature.QueryString; + if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal)) + { + _original = current; + + var result = QueryHelpers.ParseNullableQuery(current); + + if (result == null) + { + _parsedValues = QueryCollection.Empty; + } + else + { + _parsedValues = new QueryCollection(result); + } + } + return _parsedValues; + } + set + { + _parsedValues = value; + if (_features.Collection != null) + { + if (value == null) + { + _original = string.Empty; + HttpRequestFeature.QueryString = string.Empty; + } + else + { + _original = QueryString.Create(_parsedValues).ToString(); + HttpRequestFeature.QueryString = _original; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/RequestCookiesFeature.cs b/src/Http/Http/src/Features/RequestCookiesFeature.cs new file mode 100644 index 0000000000..cd37b360a4 --- /dev/null +++ b/src/Http/Http/src/Features/RequestCookiesFeature.cs @@ -0,0 +1,96 @@ +// 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.Internal; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class RequestCookiesFeature : IRequestCookiesFeature + { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullRequestFeature = f => null; + + private FeatureReferences _features; + private StringValues _original; + private IRequestCookieCollection _parsedValues; + + public RequestCookiesFeature(IRequestCookieCollection cookies) + { + if (cookies == null) + { + throw new ArgumentNullException(nameof(cookies)); + } + + _parsedValues = cookies; + } + + public RequestCookiesFeature(IFeatureCollection features) + { + if (features == null) + { + throw new ArgumentNullException(nameof(features)); + } + + _features = new FeatureReferences(features); + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache, _nullRequestFeature); + + public IRequestCookieCollection Cookies + { + get + { + if (_features.Collection == null) + { + if (_parsedValues == null) + { + _parsedValues = RequestCookieCollection.Empty; + } + return _parsedValues; + } + + var headers = HttpRequestFeature.Headers; + StringValues current; + if (!headers.TryGetValue(HeaderNames.Cookie, out current)) + { + current = string.Empty; + } + + if (_parsedValues == null || _original != current) + { + _original = current; + _parsedValues = RequestCookieCollection.Parse(current.ToArray()); + } + + return _parsedValues; + } + set + { + _parsedValues = value; + _original = StringValues.Empty; + if (_features.Collection != null) + { + if (_parsedValues == null || _parsedValues.Count == 0) + { + HttpRequestFeature.Headers.Remove(HeaderNames.Cookie); + } + else + { + var headers = new List(); + foreach (var pair in _parsedValues) + { + headers.Add(new CookieHeaderValue(pair.Key, pair.Value).ToString()); + } + _original = headers.ToArray(); + HttpRequestFeature.Headers[HeaderNames.Cookie] = _original; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/ResponseCookiesFeature.cs b/src/Http/Http/src/Features/ResponseCookiesFeature.cs new file mode 100644 index 0000000000..0d9444b0f5 --- /dev/null +++ b/src/Http/Http/src/Features/ResponseCookiesFeature.cs @@ -0,0 +1,69 @@ +// 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 Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Default implementation of . + /// + public class ResponseCookiesFeature : IResponseCookiesFeature + { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullResponseFeature = f => null; + + private FeatureReferences _features; + private IResponseCookies _cookiesCollection; + + /// + /// Initializes a new instance. + /// + /// + /// containing all defined features, including this + /// and the . + /// + public ResponseCookiesFeature(IFeatureCollection features) + : this(features, builderPool: null) + { + } + + /// + /// Initializes a new instance. + /// + /// + /// containing all defined features, including this + /// and the . + /// + /// The , if available. + public ResponseCookiesFeature(IFeatureCollection features, ObjectPool builderPool) + { + if (features == null) + { + throw new ArgumentNullException(nameof(features)); + } + + _features = new FeatureReferences(features); + } + + private IHttpResponseFeature HttpResponseFeature => _features.Fetch(ref _features.Cache, _nullResponseFeature); + + /// + public IResponseCookies Cookies + { + get + { + if (_cookiesCollection == null) + { + var headers = HttpResponseFeature.Headers; + _cookiesCollection = new ResponseCookies(headers, null); + } + + return _cookiesCollection; + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/ServiceProvidersFeature.cs b/src/Http/Http/src/Features/ServiceProvidersFeature.cs new file mode 100644 index 0000000000..d1cf4e6cba --- /dev/null +++ b/src/Http/Http/src/Features/ServiceProvidersFeature.cs @@ -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. + +using System; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class ServiceProvidersFeature : IServiceProvidersFeature + { + public IServiceProvider RequestServices { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Features/TlsConnectionFeature.cs b/src/Http/Http/src/Features/TlsConnectionFeature.cs new file mode 100644 index 0000000000..f9bfcdef7f --- /dev/null +++ b/src/Http/Http/src/Features/TlsConnectionFeature.cs @@ -0,0 +1,19 @@ +// 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.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class TlsConnectionFeature : ITlsConnectionFeature + { + public X509Certificate2 ClientCertificate { get; set; } + + public Task GetClientCertificateAsync(CancellationToken cancellationToken) + { + return Task.FromResult(ClientCertificate); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/FormCollection.cs b/src/Http/Http/src/FormCollection.cs new file mode 100644 index 0000000000..23709b2bb0 --- /dev/null +++ b/src/Http/Http/src/FormCollection.cs @@ -0,0 +1,228 @@ +// 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; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Contains the parsed form values. + /// + public class FormCollection : IFormCollection + { + public static readonly FormCollection Empty = new FormCollection(); + private static readonly string[] EmptyKeys = Array.Empty(); + private static readonly StringValues[] EmptyValues = Array.Empty(); + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private static IFormFileCollection EmptyFiles = new FormFileCollection(); + + private IFormFileCollection _files; + + private FormCollection() + { + // For static Empty + } + + public FormCollection(Dictionary fields, IFormFileCollection files = null) + { + // can be null + Store = fields; + _files = files; + } + + public IFormFileCollection Files + { + get + { + return _files ?? EmptyFiles; + } + private set { _files = value; } + } + + private Dictionary Store { get; set; } + + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] + { + get + { + if (Store == null) + { + return StringValues.Empty; + } + + StringValues value; + if (TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; + } + } + + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count + { + get + { + return Store?.Count ?? 0; + } + } + + public ICollection Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + /// + /// Retrieves a value from the dictionary. + /// + /// The header name. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an struct enumerator that iterates through a collection without boxing and is also used via the interface. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + // Non-boxed Enumerator + return new Enumerator(Store.GetEnumerator()); + } + + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + // Boxed Enumerator + return Store.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + // Boxed Enumerator + return Store.GetEnumerator(); + } + + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair); + } + } + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} diff --git a/src/Http/Http/src/HeaderDictionary.cs b/src/Http/Http/src/HeaderDictionary.cs new file mode 100644 index 0000000000..bc0b7a26ce --- /dev/null +++ b/src/Http/Http/src/HeaderDictionary.cs @@ -0,0 +1,416 @@ +// 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; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents a wrapper for RequestHeaders and ResponseHeaders. + /// + public class HeaderDictionary : IHeaderDictionary + { + private static readonly string[] EmptyKeys = Array.Empty(); + private static readonly StringValues[] EmptyValues = Array.Empty(); + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + public HeaderDictionary() + { + } + + public HeaderDictionary(Dictionary store) + { + Store = store; + } + + public HeaderDictionary(int capacity) + { + EnsureStore(capacity); + } + + private Dictionary Store { get; set; } + + private void EnsureStore(int capacity) + { + if (Store == null) + { + Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); + } + } + + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] + { + get + { + if (Store == null) + { + return StringValues.Empty; + } + + StringValues value; + if (TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; + } + set + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + ThrowIfReadOnly(); + + if (StringValues.IsNullOrEmpty(value)) + { + Store?.Remove(key); + } + else + { + EnsureStore(1); + Store[key] = value; + } + } + } + + /// + /// Throws KeyNotFoundException if the key is not present. + /// + /// The header name. + /// + StringValues IDictionary.this[string key] + { + get { return Store[key]; } + set + { + ThrowIfReadOnly(); + this[key] = value; + } + } + + public long? ContentLength + { + get + { + long value; + var rawValue = this[HeaderNames.ContentLength]; + if (rawValue.Count == 1 && + !string.IsNullOrEmpty(rawValue[0]) && + HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value)) + { + return value; + } + + return null; + } + set + { + ThrowIfReadOnly(); + if (value.HasValue) + { + this[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(value.Value); + } + else + { + this.Remove(HeaderNames.ContentLength); + } + } + } + + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count => Store?.Count ?? 0; + + /// + /// Gets a value that indicates whether the is in read-only mode. + /// + /// true if the is in read-only mode; otherwise, false. + public bool IsReadOnly { get; set; } + + public ICollection Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + public ICollection Values + { + get + { + if (Store == null) + { + return EmptyValues; + } + return Store.Values; + } + } + + /// + /// Adds a new list of items to the collection. + /// + /// The item to add. + public void Add(KeyValuePair item) + { + if (item.Key == null) + { + throw new ArgumentNullException("The key is null"); + } + ThrowIfReadOnly(); + EnsureStore(1); + Store.Add(item.Key, item.Value); + } + + /// + /// Adds the given header and values to the collection. + /// + /// The header name. + /// The header values. + public void Add(string key, StringValues value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + ThrowIfReadOnly(); + EnsureStore(1); + Store.Add(key, value); + } + + /// + /// Clears the entire list of objects. + /// + public void Clear() + { + ThrowIfReadOnly(); + Store?.Clear(); + } + + /// + /// Returns a value indicating whether the specified object occurs within this collection. + /// + /// The item. + /// true if the specified object occurs within this collection; otherwise, false. + public bool Contains(KeyValuePair item) + { + StringValues value; + if (Store == null || + !Store.TryGetValue(item.Key, out value) || + !StringValues.Equals(value, item.Value)) + { + return false; + } + return true; + } + + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + /// + /// Copies the elements to a one-dimensional Array instance at the specified index. + /// + /// The one-dimensional Array that is the destination of the specified objects copied from the . + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (Store == null) + { + return; + } + + foreach (var item in Store) + { + array[arrayIndex] = item; + arrayIndex++; + } + } + + /// + /// Removes the given item from the the collection. + /// + /// The item. + /// true if the specified object was removed from the collection; otherwise, false. + public bool Remove(KeyValuePair item) + { + ThrowIfReadOnly(); + if (Store == null) + { + return false; + } + + StringValues value; + + if (Store.TryGetValue(item.Key, out value) && StringValues.Equals(item.Value, value)) + { + return Store.Remove(item.Key); + } + return false; + } + + /// + /// Removes the given header from the collection. + /// + /// The header name. + /// true if the specified object was removed from the collection; otherwise, false. + public bool Remove(string key) + { + ThrowIfReadOnly(); + if (Store == null) + { + return false; + } + return Store.Remove(key); + } + + /// + /// Retrieves a value from the dictionary. + /// + /// The header name. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + return new Enumerator(Store.GetEnumerator()); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + return Store.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + return Store.GetEnumerator(); + } + + private void ThrowIfReadOnly() + { + if (IsReadOnly) + { + throw new InvalidOperationException("The response headers cannot be modified because the response has already started."); + } + } + + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair); + } + } + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} diff --git a/src/Http/Http/src/HttpContextAccessor.cs b/src/Http/Http/src/HttpContextAccessor.cs new file mode 100644 index 0000000000..5a4676234c --- /dev/null +++ b/src/Http/Http/src/HttpContextAccessor.cs @@ -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.Threading; + +namespace Microsoft.AspNetCore.Http +{ + public class HttpContextAccessor : IHttpContextAccessor + { + private static AsyncLocal _httpContextCurrent = new AsyncLocal(); + + public HttpContext HttpContext + { + get + { + return _httpContextCurrent.Value; + } + set + { + _httpContextCurrent.Value = value; + } + } + } +} diff --git a/src/Http/Http/src/HttpContextFactory.cs b/src/Http/Http/src/HttpContextFactory.cs new file mode 100644 index 0000000000..8236a388a5 --- /dev/null +++ b/src/Http/Http/src/HttpContextFactory.cs @@ -0,0 +1,58 @@ +// 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.Features; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Http +{ + public class HttpContextFactory : IHttpContextFactory + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly FormOptions _formOptions; + + public HttpContextFactory(IOptions formOptions) + : this(formOptions, httpContextAccessor: null) + { + } + + public HttpContextFactory(IOptions formOptions, IHttpContextAccessor httpContextAccessor) + { + if (formOptions == null) + { + throw new ArgumentNullException(nameof(formOptions)); + } + + _formOptions = formOptions.Value; + _httpContextAccessor = httpContextAccessor; + } + + public HttpContext Create(IFeatureCollection featureCollection) + { + if (featureCollection == null) + { + throw new ArgumentNullException(nameof(featureCollection)); + } + + var httpContext = new DefaultHttpContext(featureCollection); + if (_httpContextAccessor != null) + { + _httpContextAccessor.HttpContext = httpContext; + } + + var formFeature = new FormFeature(httpContext.Request, _formOptions); + featureCollection.Set(formFeature); + + return httpContext; + } + + public void Dispose(HttpContext httpContext) + { + if (_httpContextAccessor != null) + { + _httpContextAccessor.HttpContext = null; + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/HttpServiceCollectionExtensions.cs b/src/Http/Http/src/HttpServiceCollectionExtensions.cs new file mode 100644 index 0000000000..cccfe6d4e6 --- /dev/null +++ b/src/Http/Http/src/HttpServiceCollectionExtensions.cs @@ -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.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for configuring HttpContext services. + /// + public static class HttpServiceCollectionExtensions + { + /// + /// Adds a default implementation for the service. + /// + /// The . + /// The service collection. + public static IServiceCollection AddHttpContextAccessor(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddSingleton(); + return services; + } + } +} diff --git a/src/Http/Http/src/Internal/ApplicationBuilder.cs b/src/Http/Http/src/Internal/ApplicationBuilder.cs new file mode 100644 index 0000000000..d0b6b6f6bf --- /dev/null +++ b/src/Http/Http/src/Internal/ApplicationBuilder.cs @@ -0,0 +1,96 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Builder.Internal +{ + public class ApplicationBuilder : IApplicationBuilder + { + private readonly IList> _components = new List>(); + + public ApplicationBuilder(IServiceProvider serviceProvider) + { + Properties = new Dictionary(StringComparer.Ordinal); + ApplicationServices = serviceProvider; + } + + public ApplicationBuilder(IServiceProvider serviceProvider, object server) + : this(serviceProvider) + { + SetProperty(Constants.BuilderProperties.ServerFeatures, server); + } + + private ApplicationBuilder(ApplicationBuilder builder) + { + Properties = new CopyOnWriteDictionary(builder.Properties, StringComparer.Ordinal); + } + + public IServiceProvider ApplicationServices + { + get + { + return GetProperty(Constants.BuilderProperties.ApplicationServices); + } + set + { + SetProperty(Constants.BuilderProperties.ApplicationServices, value); + } + } + + public IFeatureCollection ServerFeatures + { + get + { + return GetProperty(Constants.BuilderProperties.ServerFeatures); + } + } + + public IDictionary Properties { get; } + + private T GetProperty(string key) + { + object value; + return Properties.TryGetValue(key, out value) ? (T)value : default(T); + } + + private void SetProperty(string key, T value) + { + Properties[key] = value; + } + + public IApplicationBuilder Use(Func middleware) + { + _components.Add(middleware); + return this; + } + + public IApplicationBuilder New() + { + return new ApplicationBuilder(this); + } + + public RequestDelegate Build() + { + RequestDelegate app = context => + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + }; + + foreach (var component in _components.Reverse()) + { + app = component(app); + } + + return app; + } + } +} diff --git a/src/Http/Http/src/Internal/BindingAddress.cs b/src/Http/Http/src/Internal/BindingAddress.cs new file mode 100644 index 0000000000..492fa23dbe --- /dev/null +++ b/src/Http/Http/src/Internal/BindingAddress.cs @@ -0,0 +1,155 @@ +// 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; +using System.Globalization; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class BindingAddress + { + public string Host { get; private set; } + public string PathBase { get; private set; } + public int Port { get; internal set; } + public string Scheme { get; private set; } + + public bool IsUnixPipe + { + get + { + return Host.StartsWith(Constants.UnixPipeHostPrefix, StringComparison.Ordinal); + } + } + + public string UnixPipePath + { + get + { + if (!IsUnixPipe) + { + throw new InvalidOperationException("Binding address is not a unix pipe."); + } + + return Host.Substring(Constants.UnixPipeHostPrefix.Length - 1); + } + } + + public override string ToString() + { + if (IsUnixPipe) + { + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant(); + } + else + { + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture) + PathBase.ToString(CultureInfo.InvariantCulture); + } + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public override bool Equals(object obj) + { + var other = obj as BindingAddress; + if (other == null) + { + return false; + } + return string.Equals(Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) + && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase) + && Port == other.Port + && PathBase == other.PathBase; + } + + public static BindingAddress Parse(string address) + { + address = address ?? string.Empty; + + int schemeDelimiterStart = address.IndexOf("://", StringComparison.Ordinal); + if (schemeDelimiterStart < 0) + { + throw new FormatException($"Invalid url: '{address}'"); + } + int schemeDelimiterEnd = schemeDelimiterStart + "://".Length; + + var isUnixPipe = address.IndexOf(Constants.UnixPipeHostPrefix, schemeDelimiterEnd, StringComparison.Ordinal) == schemeDelimiterEnd; + + int pathDelimiterStart; + int pathDelimiterEnd; + if (!isUnixPipe) + { + pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart; + } + else + { + pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + Constants.UnixPipeHostPrefix.Length, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart + ":".Length; + } + + if (pathDelimiterStart < 0) + { + pathDelimiterStart = pathDelimiterEnd = address.Length; + } + + var serverAddress = new BindingAddress(); + serverAddress.Scheme = address.Substring(0, schemeDelimiterStart); + + var hasSpecifiedPort = false; + if (!isUnixPipe) + { + int portDelimiterStart = address.LastIndexOf(":", pathDelimiterStart - 1, pathDelimiterStart - schemeDelimiterEnd, StringComparison.Ordinal); + if (portDelimiterStart >= 0) + { + int portDelimiterEnd = portDelimiterStart + ":".Length; + + string portString = address.Substring(portDelimiterEnd, pathDelimiterStart - portDelimiterEnd); + int portNumber; + if (int.TryParse(portString, NumberStyles.Integer, CultureInfo.InvariantCulture, out portNumber)) + { + hasSpecifiedPort = true; + serverAddress.Host = address.Substring(schemeDelimiterEnd, portDelimiterStart - schemeDelimiterEnd); + serverAddress.Port = portNumber; + } + } + + if (!hasSpecifiedPort) + { + if (string.Equals(serverAddress.Scheme, "http", StringComparison.OrdinalIgnoreCase)) + { + serverAddress.Port = 80; + } + else if (string.Equals(serverAddress.Scheme, "https", StringComparison.OrdinalIgnoreCase)) + { + serverAddress.Port = 443; + } + } + } + + if (!hasSpecifiedPort) + { + serverAddress.Host = address.Substring(schemeDelimiterEnd, pathDelimiterStart - schemeDelimiterEnd); + } + + if (string.IsNullOrEmpty(serverAddress.Host)) + { + throw new FormatException($"Invalid url: '{address}'"); + } + + if (address[address.Length - 1] == '/') + { + serverAddress.PathBase = address.Substring(pathDelimiterEnd, address.Length - pathDelimiterEnd - 1); + } + else + { + serverAddress.PathBase = address.Substring(pathDelimiterEnd); + } + + return serverAddress; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/BufferingHelper.cs b/src/Http/Http/src/Internal/BufferingHelper.cs new file mode 100644 index 0000000000..b912f37116 --- /dev/null +++ b/src/Http/Http/src/Internal/BufferingHelper.cs @@ -0,0 +1,80 @@ +// 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.IO; +using Microsoft.AspNetCore.WebUtilities; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public static class BufferingHelper + { + internal const int DefaultBufferThreshold = 1024 * 30; + + private readonly static Func _getTempDirectory = () => TempDirectory; + + private static string _tempDirectory; + + public static string TempDirectory + { + get + { + if (_tempDirectory == null) + { + // Look for folders in the following order. + var temp = Environment.GetEnvironmentVariable("ASPNETCORE_TEMP") ?? // ASPNETCORE_TEMP - User set temporary location. + Path.GetTempPath(); // Fall back. + + if (!Directory.Exists(temp)) + { + // TODO: ??? + throw new DirectoryNotFoundException(temp); + } + + _tempDirectory = temp; + } + + return _tempDirectory; + } + } + + public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + var body = request.Body; + if (!body.CanSeek) + { + var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory); + request.Body = fileStream; + request.HttpContext.Response.RegisterForDispose(fileStream); + } + return request; + } + + public static MultipartSection EnableRewind(this MultipartSection section, Action registerForDispose, + int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); + } + if (registerForDispose == null) + { + throw new ArgumentNullException(nameof(registerForDispose)); + } + + var body = section.Body; + if (!body.CanSeek) + { + var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory); + section.Body = fileStream; + registerForDispose(fileStream); + } + return section; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/Constants.cs b/src/Http/Http/src/Internal/Constants.cs new file mode 100644 index 0000000000..280011b3e0 --- /dev/null +++ b/src/Http/Http/src/Internal/Constants.cs @@ -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.AspNetCore.Http.Internal +{ + internal static class Constants + { + internal const string Http = "http"; + internal const string Https = "https"; + internal const string UnixPipeHostPrefix = "unix:/"; + + internal static class BuilderProperties + { + internal static string ServerFeatures = "server.Features"; + internal static string ApplicationServices = "application.Services"; + } + } +} diff --git a/src/Http/Http/src/Internal/DefaultConnectionInfo.cs b/src/Http/Http/src/Internal/DefaultConnectionInfo.cs new file mode 100644 index 0000000000..6ae7f9fc38 --- /dev/null +++ b/src/Http/Http/src/Internal/DefaultConnectionInfo.cs @@ -0,0 +1,90 @@ +// 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.Net; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultConnectionInfo : ConnectionInfo + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _newHttpConnectionFeature = f => new HttpConnectionFeature(); + private readonly static Func _newTlsConnectionFeature = f => new TlsConnectionFeature(); + + private FeatureReferences _features; + + public DefaultConnectionInfo(IFeatureCollection features) + { + Initialize(features); + } + + public virtual void Initialize( IFeatureCollection features) + { + _features = new FeatureReferences(features); + } + + public virtual void Uninitialize() + { + _features = default(FeatureReferences); + } + + private IHttpConnectionFeature HttpConnectionFeature => + _features.Fetch(ref _features.Cache.Connection, _newHttpConnectionFeature); + + private ITlsConnectionFeature TlsConnectionFeature=> + _features.Fetch(ref _features.Cache.TlsConnection, _newTlsConnectionFeature); + + /// + public override string Id + { + get { return HttpConnectionFeature.ConnectionId; } + set { HttpConnectionFeature.ConnectionId = value; } + } + + public override IPAddress RemoteIpAddress + { + get { return HttpConnectionFeature.RemoteIpAddress; } + set { HttpConnectionFeature.RemoteIpAddress = value; } + } + + public override int RemotePort + { + get { return HttpConnectionFeature.RemotePort; } + set { HttpConnectionFeature.RemotePort = value; } + } + + public override IPAddress LocalIpAddress + { + get { return HttpConnectionFeature.LocalIpAddress; } + set { HttpConnectionFeature.LocalIpAddress = value; } + } + + public override int LocalPort + { + get { return HttpConnectionFeature.LocalPort; } + set { HttpConnectionFeature.LocalPort = value; } + } + + public override X509Certificate2 ClientCertificate + { + get { return TlsConnectionFeature.ClientCertificate; } + set { TlsConnectionFeature.ClientCertificate = value; } + } + + public override Task GetClientCertificateAsync(CancellationToken cancellationToken = new CancellationToken()) + { + return TlsConnectionFeature.GetClientCertificateAsync(cancellationToken); + } + + struct FeatureInterfaces + { + public IHttpConnectionFeature Connection; + public ITlsConnectionFeature TlsConnection; + } + } +} diff --git a/src/Http/Http/src/Internal/DefaultHttpRequest.cs b/src/Http/Http/src/Internal/DefaultHttpRequest.cs new file mode 100644 index 0000000000..f216475db6 --- /dev/null +++ b/src/Http/Http/src/Internal/DefaultHttpRequest.cs @@ -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.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultHttpRequest : HttpRequest + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullRequestFeature = f => null; + private readonly static Func _newQueryFeature = f => new QueryFeature(f); + private readonly static Func _newFormFeature = r => new FormFeature(r); + private readonly static Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); + + private HttpContext _context; + private FeatureReferences _features; + + public DefaultHttpRequest(HttpContext context) + { + Initialize(context); + } + + public virtual void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences(context.Features); + } + + public virtual void Uninitialize() + { + _context = null; + _features = default(FeatureReferences); + } + + public override HttpContext HttpContext => _context; + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature); + + private IQueryFeature QueryFeature => + _features.Fetch(ref _features.Cache.Query, _newQueryFeature); + + private IFormFeature FormFeature => + _features.Fetch(ref _features.Cache.Form, this, _newFormFeature); + + private IRequestCookiesFeature RequestCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newRequestCookiesFeature); + + public override PathString PathBase + { + get { return new PathString(HttpRequestFeature.PathBase); } + set { HttpRequestFeature.PathBase = value.Value; } + } + + public override PathString Path + { + get { return new PathString(HttpRequestFeature.Path); } + set { HttpRequestFeature.Path = value.Value; } + } + + public override QueryString QueryString + { + get { return new QueryString(HttpRequestFeature.QueryString); } + set { HttpRequestFeature.QueryString = value.Value; } + } + + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override Stream Body + { + get { return HttpRequestFeature.Body; } + set { HttpRequestFeature.Body = value; } + } + + public override string Method + { + get { return HttpRequestFeature.Method; } + set { HttpRequestFeature.Method = value; } + } + + public override string Scheme + { + get { return HttpRequestFeature.Scheme; } + set { HttpRequestFeature.Scheme = value; } + } + + public override bool IsHttps + { + get { return string.Equals(Constants.Https, Scheme, StringComparison.OrdinalIgnoreCase); } + set { Scheme = value ? Constants.Https : Constants.Http; } + } + + public override HostString Host + { + get { return HostString.FromUriComponent(Headers["Host"]); } + set { Headers["Host"] = value.ToUriComponent(); } + } + + public override IQueryCollection Query + { + get { return QueryFeature.Query; } + set { QueryFeature.Query = value; } + } + + public override string Protocol + { + get { return HttpRequestFeature.Protocol; } + set { HttpRequestFeature.Protocol = value; } + } + + public override IHeaderDictionary Headers + { + get { return HttpRequestFeature.Headers; } + } + + public override IRequestCookieCollection Cookies + { + get { return RequestCookiesFeature.Cookies; } + set { RequestCookiesFeature.Cookies = value; } + } + + public override string ContentType + { + get { return Headers[HeaderNames.ContentType]; } + set { Headers[HeaderNames.ContentType] = value; } + } + + public override bool HasFormContentType + { + get { return FormFeature.HasFormContentType; } + } + + public override IFormCollection Form + { + get { return FormFeature.ReadForm(); } + set { FormFeature.Form = value; } + } + + public override Task ReadFormAsync(CancellationToken cancellationToken) + { + return FormFeature.ReadFormAsync(cancellationToken); + } + + struct FeatureInterfaces + { + public IHttpRequestFeature Request; + public IQueryFeature Query; + public IFormFeature Form; + public IRequestCookiesFeature Cookies; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/DefaultHttpResponse.cs b/src/Http/Http/src/Internal/DefaultHttpResponse.cs new file mode 100644 index 0000000000..3ca05035f5 --- /dev/null +++ b/src/Http/Http/src/Internal/DefaultHttpResponse.cs @@ -0,0 +1,139 @@ +// 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.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultHttpResponse : HttpResponse + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullResponseFeature = f => null; + private readonly static Func _newResponseCookiesFeature = f => new ResponseCookiesFeature(f); + + private HttpContext _context; + private FeatureReferences _features; + + public DefaultHttpResponse(HttpContext context) + { + Initialize(context); + } + + public virtual void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences(context.Features); + } + + public virtual void Uninitialize() + { + _context = null; + _features = default(FeatureReferences); + } + + private IHttpResponseFeature HttpResponseFeature => + _features.Fetch(ref _features.Cache.Response, _nullResponseFeature); + + private IResponseCookiesFeature ResponseCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature); + + + public override HttpContext HttpContext { get { return _context; } } + + public override int StatusCode + { + get { return HttpResponseFeature.StatusCode; } + set { HttpResponseFeature.StatusCode = value; } + } + + public override IHeaderDictionary Headers + { + get { return HttpResponseFeature.Headers; } + } + + public override Stream Body + { + get { return HttpResponseFeature.Body; } + set { HttpResponseFeature.Body = value; } + } + + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override string ContentType + { + get + { + return Headers[HeaderNames.ContentType]; + } + set + { + if (string.IsNullOrEmpty(value)) + { + HttpResponseFeature.Headers.Remove(HeaderNames.ContentType); + } + else + { + HttpResponseFeature.Headers[HeaderNames.ContentType] = value; + } + } + } + + public override IResponseCookies Cookies + { + get { return ResponseCookiesFeature.Cookies; } + } + + public override bool HasStarted + { + get { return HttpResponseFeature.HasStarted; } + } + + public override void OnStarting(Func callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + HttpResponseFeature.OnStarting(callback, state); + } + + public override void OnCompleted(Func callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + HttpResponseFeature.OnCompleted(callback, state); + } + + public override void Redirect(string location, bool permanent) + { + if (permanent) + { + HttpResponseFeature.StatusCode = 301; + } + else + { + HttpResponseFeature.StatusCode = 302; + } + + Headers[HeaderNames.Location] = location; + } + + struct FeatureInterfaces + { + public IHttpResponseFeature Response; + public IResponseCookiesFeature Cookies; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/DefaultWebSocketManager.cs b/src/Http/Http/src/Internal/DefaultWebSocketManager.cs new file mode 100644 index 0000000000..477282408d --- /dev/null +++ b/src/Http/Http/src/Internal/DefaultWebSocketManager.cs @@ -0,0 +1,73 @@ +// 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.Net.WebSockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultWebSocketManager : WebSocketManager + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullRequestFeature = f => null; + private readonly static Func _nullWebSocketFeature = f => null; + + private FeatureReferences _features; + + public DefaultWebSocketManager(IFeatureCollection features) + { + Initialize(features); + } + + public virtual void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences(features); + } + + public virtual void Uninitialize() + { + _features = default(FeatureReferences); + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature); + + private IHttpWebSocketFeature WebSocketFeature => + _features.Fetch(ref _features.Cache.WebSockets, _nullWebSocketFeature); + + public override bool IsWebSocketRequest + { + get + { + return WebSocketFeature != null && WebSocketFeature.IsWebSocketRequest; + } + } + + public override IList WebSocketRequestedProtocols + { + get + { + return ParsingHelpers.GetHeaderSplit(HttpRequestFeature.Headers, HeaderNames.WebSocketSubProtocols); + } + } + + public override Task AcceptWebSocketAsync(string subProtocol) + { + if (WebSocketFeature == null) + { + throw new NotSupportedException("WebSockets are not supported"); + } + return WebSocketFeature.AcceptAsync(new WebSocketAcceptContext() { SubProtocol = subProtocol }); + } + + struct FeatureInterfaces + { + public IHttpRequestFeature Request; + public IHttpWebSocketFeature WebSockets; + } + } +} diff --git a/src/Http/Http/src/Internal/FormFile.cs b/src/Http/Http/src/Internal/FormFile.cs new file mode 100644 index 0000000000..b4a3f4d91f --- /dev/null +++ b/src/Http/Http/src/Internal/FormFile.cs @@ -0,0 +1,109 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class FormFile : IFormFile + { + // Stream.CopyTo method uses 80KB as the default buffer size. + private const int DefaultBufferSize = 80 * 1024; + + private readonly Stream _baseStream; + private readonly long _baseStreamOffset; + + public FormFile(Stream baseStream, long baseStreamOffset, long length, string name, string fileName) + { + _baseStream = baseStream; + _baseStreamOffset = baseStreamOffset; + Length = length; + Name = name; + FileName = fileName; + } + + /// + /// Gets the raw Content-Disposition header of the uploaded file. + /// + public string ContentDisposition + { + get { return Headers["Content-Disposition"]; } + set { Headers["Content-Disposition"] = value; } + } + + /// + /// Gets the raw Content-Type header of the uploaded file. + /// + public string ContentType + { + get { return Headers["Content-Type"]; } + set { Headers["Content-Type"] = value; } + } + + /// + /// Gets the header dictionary of the uploaded file. + /// + public IHeaderDictionary Headers { get; set; } + + /// + /// Gets the file length in bytes. + /// + public long Length { get; } + + /// + /// Gets the name from the Content-Disposition header. + /// + public string Name { get; } + + /// + /// Gets the file name from the Content-Disposition header. + /// + public string FileName { get; } + + /// + /// Opens the request stream for reading the uploaded file. + /// + public Stream OpenReadStream() + { + return new ReferenceReadStream(_baseStream, _baseStreamOffset, Length); + } + + /// + /// Copies the contents of the uploaded file to the stream. + /// + /// The stream to copy the file contents to. + public void CopyTo(Stream target) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + using (var readStream = OpenReadStream()) + { + readStream.CopyTo(target, DefaultBufferSize); + } + } + + /// + /// Asynchronously copies the contents of the uploaded file to the stream. + /// + /// The stream to copy the file contents to. + /// + public async Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken)) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + using (var readStream = OpenReadStream()) + { + await readStream.CopyToAsync(target, DefaultBufferSize, cancellationToken); + } + } + } +} diff --git a/src/Http/Http/src/Internal/FormFileCollection.cs b/src/Http/Http/src/Internal/FormFileCollection.cs new file mode 100644 index 0000000000..806e756a8e --- /dev/null +++ b/src/Http/Http/src/Internal/FormFileCollection.cs @@ -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 System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class FormFileCollection : List, IFormFileCollection + { + public IFormFile this[string name] => GetFile(name); + + public IFormFile GetFile(string name) + { + foreach (var file in this) + { + if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase)) + { + return file; + } + } + + return null; + } + + public IReadOnlyList GetFiles(string name) + { + var files = new List(); + + foreach (var file in this) + { + if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase)) + { + files.Add(file); + } + } + + return files; + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/ItemsDictionary.cs b/src/Http/Http/src/Internal/ItemsDictionary.cs new file mode 100644 index 0000000000..4821912240 --- /dev/null +++ b/src/Http/Http/src/Internal/ItemsDictionary.cs @@ -0,0 +1,118 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class ItemsDictionary : IDictionary + { + public ItemsDictionary() + : this(new Dictionary()) + { + } + + public ItemsDictionary(IDictionary items) + { + Items = items; + } + + public IDictionary Items { get; } + + // Replace the indexer with one that returns null for missing values + object IDictionary.this[object key] + { + get + { + object value; + if (Items.TryGetValue(key, out value)) + { + return value; + } + return null; + } + set { Items[key] = value; } + } + + void IDictionary.Add(object key, object value) + { + Items.Add(key, value); + } + + bool IDictionary.ContainsKey(object key) + { + return Items.ContainsKey(key); + } + + ICollection IDictionary.Keys + { + get { return Items.Keys; } + } + + bool IDictionary.Remove(object key) + { + return Items.Remove(key); + } + + bool IDictionary.TryGetValue(object key, out object value) + { + return Items.TryGetValue(key, out value); + } + + ICollection IDictionary.Values + { + get { return Items.Values; } + } + + void ICollection>.Add(KeyValuePair item) + { + Items.Add(item); + } + + void ICollection>.Clear() + { + Items.Clear(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return Items.Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + Items.CopyTo(array, arrayIndex); + } + + int ICollection>.Count + { + get { return Items.Count; } + } + + bool ICollection>.IsReadOnly + { + get { return Items.IsReadOnly; } + } + + bool ICollection>.Remove(KeyValuePair item) + { + object value; + if (Items.TryGetValue(item.Key, out value) && Equals(item.Value, value)) + { + return Items.Remove(item.Key); + } + return false; + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return Items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Items.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/QueryCollection.cs b/src/Http/Http/src/Internal/QueryCollection.cs new file mode 100644 index 0000000000..620de44a92 --- /dev/null +++ b/src/Http/Http/src/Internal/QueryCollection.cs @@ -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; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http.Internal +{ + /// + /// The HttpRequest query string collection + /// + public class QueryCollection : IQueryCollection + { + public static readonly QueryCollection Empty = new QueryCollection(); + private static readonly string[] EmptyKeys = Array.Empty(); + private static readonly StringValues[] EmptyValues = Array.Empty(); + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private Dictionary Store { get; set; } + + public QueryCollection() + { + } + + public QueryCollection(Dictionary store) + { + Store = store; + } + + public QueryCollection(QueryCollection store) + { + Store = store.Store; + } + + public QueryCollection(int capacity) + { + Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The key name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] + { + get + { + if (Store == null) + { + return StringValues.Empty; + } + + StringValues value; + if (TryGetValue(key, out value)) + { + return value; + } + return StringValues.Empty; + } + } + + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count + { + get + { + if (Store == null) + { + return 0; + } + return Store.Count; + } + } + + public ICollection Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + /// + /// Retrieves a value from the collection. + /// + /// The key. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + return new Enumerator(Store.GetEnumerator()); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + return Store.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + return Store.GetEnumerator(); + } + + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair); + } + } + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} diff --git a/src/Http/Http/src/Internal/ReferenceReadStream.cs b/src/Http/Http/src/Internal/ReferenceReadStream.cs new file mode 100644 index 0000000000..c36a59d010 --- /dev/null +++ b/src/Http/Http/src/Internal/ReferenceReadStream.cs @@ -0,0 +1,154 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Internal +{ + /// + /// A Stream that wraps another stream starting at a certain offset and reading for the given length. + /// + internal class ReferenceReadStream : Stream + { + private readonly Stream _inner; + private readonly long _innerOffset; + private readonly long _length; + private long _position; + + private bool _disposed; + + public ReferenceReadStream(Stream inner, long offset, long length) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + _inner = inner; + _innerOffset = offset; + _length = length; + _inner.Position = offset; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return _inner.CanSeek; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + set + { + ThrowIfDisposed(); + if (value < 0 || value > Length) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be within the length of the Stream: " + Length.ToString()); + } + VerifyPosition(); + _position = value; + _inner.Position = _innerOffset + _position; + } + } + + // Throws if the position in the underlying stream has changed without our knowledge, indicating someone else is trying + // to use the stream at the same time which could lead to data corruption. + private void VerifyPosition() + { + if (_inner.Position != _innerOffset + _position) + { + throw new InvalidOperationException("The inner stream position has changed unexpectedly."); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + else // if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + return Position; + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + VerifyPosition(); + var toRead = Math.Min(count, _length - _position); + var read = _inner.Read(buffer, offset, (int)toRead); + _position += read; + return read; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + VerifyPosition(); + var toRead = Math.Min(count, _length - _position); + var read = await _inner.ReadAsync(buffer, offset, (int)toRead, cancellationToken); + _position += read; + return read; + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + } + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(ReferenceReadStream)); + } + } + } +} diff --git a/src/Http/Http/src/Internal/RequestCookieCollection.cs b/src/Http/Http/src/Internal/RequestCookieCollection.cs new file mode 100644 index 0000000000..4af0a65246 --- /dev/null +++ b/src/Http/Http/src/Internal/RequestCookieCollection.cs @@ -0,0 +1,232 @@ +// 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; +using System.Collections.Generic; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class RequestCookieCollection : IRequestCookieCollection + { + public static readonly RequestCookieCollection Empty = new RequestCookieCollection(); + private static readonly string[] EmptyKeys = Array.Empty(); + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private Dictionary Store { get; set; } + + public RequestCookieCollection() + { + } + + public RequestCookieCollection(Dictionary store) + { + Store = store; + } + + public RequestCookieCollection(int capacity) + { + Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); + } + + public string this[string key] + { + get + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (Store == null) + { + return null; + } + + string value; + if (TryGetValue(key, out value)) + { + return value; + } + return null; + } + } + + public static RequestCookieCollection Parse(IList values) + { + if (values.Count == 0) + { + return Empty; + } + + IList cookies; + if (CookieHeaderValue.TryParseList(values, out cookies)) + { + if (cookies.Count == 0) + { + return Empty; + } + + var collection = new RequestCookieCollection(cookies.Count); + var store = collection.Store; + for (var i = 0; i < cookies.Count; i++) + { + var cookie = cookies[i]; + var name = Uri.UnescapeDataString(cookie.Name.Value); + var value = Uri.UnescapeDataString(cookie.Value.Value); + store[name] = value; + } + + return collection; + } + return Empty; + } + + public int Count + { + get + { + if (Store == null) + { + return 0; + } + return Store.Count; + } + } + + public ICollection Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + public bool TryGetValue(string key, out string value) + { + if (Store == null) + { + value = null; + return false; + } + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an struct enumerator that iterates through a collection without boxing. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + // Non-boxed Enumerator + return new Enumerator(Store.GetEnumerator()); + } + + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + // Boxed Enumerator + return GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + // Boxed Enumerator + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + public KeyValuePair Current + { + get + { + if (_notEmpty) + { + var current = _dictionaryEnumerator.Current; + return new KeyValuePair(current.Key, current.Value); + } + return default(KeyValuePair); + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public void Dispose() + { + } + + public void Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/src/Internal/ResponseCookies.cs b/src/Http/Http/src/Internal/ResponseCookies.cs new file mode 100644 index 0000000000..7c6e3e033b --- /dev/null +++ b/src/Http/Http/src/Internal/ResponseCookies.cs @@ -0,0 +1,139 @@ +// 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.Text; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + /// + /// A wrapper for the response Set-Cookie header. + /// + public class ResponseCookies : IResponseCookies + { + /// + /// Create a new wrapper. + /// + /// The for the response. + /// The , if available. + public ResponseCookies(IHeaderDictionary headers, ObjectPool builderPool) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + Headers = headers; + } + + private IHeaderDictionary Headers { get; set; } + + /// + public void Append(string key, string value) + { + var setCookieHeaderValue = new SetCookieHeaderValue( + Uri.EscapeDataString(key), + Uri.EscapeDataString(value)) + { + Path = "/" + }; + var cookieValue = setCookieHeaderValue.ToString(); + + Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue); + } + + /// + public void Append(string key, string value, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var setCookieHeaderValue = new SetCookieHeaderValue( + Uri.EscapeDataString(key), + Uri.EscapeDataString(value)) + { + Domain = options.Domain, + Path = options.Path, + Expires = options.Expires, + MaxAge = options.MaxAge, + Secure = options.Secure, + SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, + HttpOnly = options.HttpOnly + }; + + var cookieValue = setCookieHeaderValue.ToString(); + + Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue); + } + + /// + public void Delete(string key) + { + Delete(key, new CookieOptions() { Path = "/" }); + } + + /// + public void Delete(string key, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var encodedKeyPlusEquals = Uri.EscapeDataString(key) + "="; + bool domainHasValue = !string.IsNullOrEmpty(options.Domain); + bool pathHasValue = !string.IsNullOrEmpty(options.Path); + + Func rejectPredicate; + if (domainHasValue) + { + rejectPredicate = (value, encKeyPlusEquals, opts) => + value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && + value.IndexOf($"domain={opts.Domain}", StringComparison.OrdinalIgnoreCase) != -1; + } + else if (pathHasValue) + { + rejectPredicate = (value, encKeyPlusEquals, opts) => + value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && + value.IndexOf($"path={opts.Path}", StringComparison.OrdinalIgnoreCase) != -1; + } + else + { + rejectPredicate = (value, encKeyPlusEquals, opts) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase); + } + + var existingValues = Headers[HeaderNames.SetCookie]; + if (!StringValues.IsNullOrEmpty(existingValues)) + { + var values = existingValues.ToArray(); + var newValues = new List(); + + for (var i = 0; i < values.Length; i++) + { + if (!rejectPredicate(values[i], encodedKeyPlusEquals, options)) + { + newValues.Add(values[i]); + } + } + + Headers[HeaderNames.SetCookie] = new StringValues(newValues.ToArray()); + } + + Append(key, string.Empty, new CookieOptions + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + Secure = options.Secure, + HttpOnly = options.HttpOnly, + SameSite = options.SameSite + }); + } + } +} diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj new file mode 100644 index 0000000000..4344d0ae8e --- /dev/null +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -0,0 +1,21 @@ + + + + ASP.NET Core default HTTP feature implementations. + netstandard2.0 + $(NoWarn);CS1591 + true + true + aspnetcore + + + + + + + + + + + + diff --git a/src/Http/Http/src/MiddlewareFactory.cs b/src/Http/Http/src/MiddlewareFactory.cs new file mode 100644 index 0000000000..5e5cd285f4 --- /dev/null +++ b/src/Http/Http/src/MiddlewareFactory.cs @@ -0,0 +1,35 @@ +// 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 System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Http +{ + public class MiddlewareFactory : IMiddlewareFactory + { + // The default middleware factory is just an IServiceProvider proxy. + // This should be registered as a scoped service so that the middleware instances + // don't end up being singletons. + private readonly IServiceProvider _serviceProvider; + + public MiddlewareFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IMiddleware Create(Type middlewareType) + { + return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware; + } + + public void Release(IMiddleware middleware) + { + // The container owns the lifetime of the service + } + } +} diff --git a/src/Http/Http/src/RequestFormReaderExtensions.cs b/src/Http/Http/src/RequestFormReaderExtensions.cs new file mode 100644 index 0000000000..8675ad7f8c --- /dev/null +++ b/src/Http/Http/src/RequestFormReaderExtensions.cs @@ -0,0 +1,48 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http +{ + public static class RequestFormReaderExtensions + { + /// + /// Read the request body as a form with the given options. These options will only be used + /// if the form has not already been read. + /// + /// The request. + /// Options for reading the form. + /// + /// The parsed form. + public static Task ReadFormAsync(this HttpRequest request, FormOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (!request.HasFormContentType) + { + throw new InvalidOperationException("Incorrect Content-Type: " + request.ContentType); + } + + var features = request.HttpContext.Features; + var formFeature = features.Get(); + if (formFeature == null || formFeature.Form == null) + { + // We haven't read the form yet, replace the reader with one using our own options. + features.Set(new FormFeature(request, options)); + } + return request.ReadFormAsync(cancellationToken); + } + } +} diff --git a/src/Http/Http/src/baseline.netcore.json b/src/Http/Http/src/baseline.netcore.json new file mode 100644 index 0000000000..932bd2b6e4 --- /dev/null +++ b/src/Http/Http/src/baseline.netcore.json @@ -0,0 +1,2783 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Http, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Http.DefaultHttpContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Http.HttpContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Initialize", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Uninitialize", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Features", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Request", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Connection", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ConnectionInfo", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Authentication", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_WebSockets", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.WebSocketManager", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Items", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequestServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequestAborted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestAborted", + "Parameters": [ + { + "Name": "value", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TraceIdentifier", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TraceIdentifier", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Session", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ISession", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Session", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.ISession" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeHttpRequest", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeHttpRequest", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeHttpResponse", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeHttpResponse", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeConnectionInfo", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ConnectionInfo", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeConnectionInfo", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.ConnectionInfo" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeAuthenticationManager", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeAuthenticationManager", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.Authentication.AuthenticationManager" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeWebSocketManager", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.WebSocketManager", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UninitializeWebSocketManager", + "Parameters": [ + { + "Name": "instance", + "Type": "Microsoft.AspNetCore.Http.WebSocketManager" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpRequestRewindExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "EnableBuffering", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnableBuffering", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "bufferThreshold", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnableBuffering", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "bufferLimit", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnableBuffering", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "bufferThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.FormCollection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IFormCollection" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Files", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormFileCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IFormCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.FormCollection+Enumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "fields", + "Type": "System.Collections.Generic.Dictionary" + }, + { + "Name": "files", + "Type": "Microsoft.AspNetCore.Http.IFormFileCollection", + "DefaultValue": "null" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Empty", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.FormCollection", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HeaderDictionary", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IHeaderDictionary" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Keys", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Values", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ContainsKey", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetValue", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "item", + "Type": "System.Collections.Generic.KeyValuePair" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Contains", + "Parameters": [ + { + "Name": "item", + "Type": "System.Collections.Generic.KeyValuePair" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CopyTo", + "Parameters": [ + { + "Name": "array", + "Type": "System.Collections.Generic.KeyValuePair[]" + }, + { + "Name": "arrayIndex", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "item", + "Type": "System.Collections.Generic.KeyValuePair" + } + ], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.ICollection>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Primitives.StringValues", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "Microsoft.Extensions.Primitives.StringValues" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentLength", + "Parameters": [], + "ReturnType": "System.Nullable", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContentLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsReadOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HeaderDictionary+Enumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "store", + "Type": "System.Collections.Generic.Dictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "capacity", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpContextAccessor", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IHttpContextAccessor" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHttpContextAccessor", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpContext", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHttpContextAccessor", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HttpContextFactory", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IHttpContextFactory" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "featureCollection", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHttpContextFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IHttpContextFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "formOptions", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "formOptions", + "Type": "Microsoft.Extensions.Options.IOptions" + }, + { + "Name": "httpContextAccessor", + "Type": "Microsoft.AspNetCore.Http.IHttpContextAccessor" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.MiddlewareFactory", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.IMiddlewareFactory" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Create", + "Parameters": [ + { + "Name": "middlewareType", + "Type": "System.Type" + } + ], + "ReturnType": "Microsoft.AspNetCore.Http.IMiddleware", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IMiddlewareFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Release", + "Parameters": [ + { + "Name": "middleware", + "Type": "Microsoft.AspNetCore.Http.IMiddleware" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.IMiddlewareFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "serviceProvider", + "Type": "System.IServiceProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.RequestFormReaderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.Features.FormOptions" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.DefaultSessionFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.ISessionFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Session", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.ISession", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ISessionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Session", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.ISession" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ISessionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FormFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IFormFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_HasFormContentType", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Form", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Form", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IFormCollection" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadForm", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IFormCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFormFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "form", + "Type": "Microsoft.AspNetCore.Http.IFormCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "request", + "Type": "Microsoft.AspNetCore.Http.HttpRequest" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.Features.FormOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.FormOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_BufferBody", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BufferBody", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MemoryBufferThreshold", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MemoryBufferThreshold", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BufferBodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BufferBodyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ValueCountLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_KeyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_KeyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ValueLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MultipartBoundaryLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MultipartBoundaryLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MultipartHeadersCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MultipartHeadersCountLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MultipartHeadersLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MultipartHeadersLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MultipartBodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MultipartBodyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultMemoryBufferThreshold", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "65536" + }, + { + "Kind": "Field", + "Name": "DefaultBufferBodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "134217728" + }, + { + "Kind": "Field", + "Name": "DefaultMultipartBoundaryLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "128" + }, + { + "Kind": "Field", + "Name": "DefaultMultipartBodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int64", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "134217728" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpConnectionFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ConnectionId", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConnectionId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LocalPort", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LocalPort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteIpAddress", + "Parameters": [], + "ReturnType": "System.Net.IPAddress", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteIpAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.IPAddress" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemotePort", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemotePort", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpRequestFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Protocol", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Protocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Scheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Method", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Method", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PathBase", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PathBase", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_QueryString", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_QueryString", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RawTarget", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RawTarget", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpRequestIdentifierFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_TraceIdentifier", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TraceIdentifier", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpRequestLifetimeFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestAborted", + "Parameters": [], + "ReturnType": "System.Threading.CancellationToken", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestAborted", + "Parameters": [ + { + "Name": "value", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.HttpResponseFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_StatusCode", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StatusCode", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReasonPhrase", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReasonPhrase", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IHeaderDictionary" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasStarted", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnStarting", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnCompleted", + "Parameters": [ + { + "Name": "callback", + "Type": "System.Func" + }, + { + "Name": "state", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ItemsFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IItemsFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Items", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IItemsFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Items", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IItemsFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.QueryFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IQueryFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Query", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IQueryCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IQueryFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Query", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IQueryCollection" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IQueryFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "query", + "Type": "Microsoft.AspNetCore.Http.IQueryCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.RequestCookiesFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IRequestCookiesFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IRequestCookieCollection", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IRequestCookiesFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookies", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.IRequestCookieCollection" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IRequestCookiesFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "cookies", + "Type": "Microsoft.AspNetCore.Http.IRequestCookieCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ResponseCookiesFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookies", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.IResponseCookies", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "features", + "Type": "Microsoft.AspNetCore.Http.Features.IFeatureCollection" + }, + { + "Name": "builderPool", + "Type": "Microsoft.Extensions.ObjectPool.ObjectPool" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.ServiceProvidersFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestServices", + "Parameters": [], + "ReturnType": "System.IServiceProvider", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestServices", + "Parameters": [ + { + "Name": "value", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.TlsConnectionFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ClientCertificate", + "Parameters": [], + "ReturnType": "System.Security.Cryptography.X509Certificates.X509Certificate2", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientCertificate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetClientCertificateAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Handler", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.Features.Authentication.IAuthenticationHandler" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.HttpServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddHttpContextAccessor", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.FormCollection+Enumerator", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerator>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MoveNext", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.IEnumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Current", + "Parameters": [], + "ReturnType": "System.Collections.Generic.KeyValuePair", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerator>", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Http.HeaderDictionary+Enumerator", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerator>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MoveNext", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.IEnumerator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Current", + "Parameters": [], + "ReturnType": "System.Collections.Generic.KeyValuePair", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerator>", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/Http/test/Authentication/DefaultAuthenticationManagerTests.cs b/src/Http/Http/test/Authentication/DefaultAuthenticationManagerTests.cs new file mode 100644 index 0000000000..101f2b19eb --- /dev/null +++ b/src/Http/Http/test/Authentication/DefaultAuthenticationManagerTests.cs @@ -0,0 +1,104 @@ +// 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. + +#pragma warning disable CS0618 // Type or member is obsolete +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Authentication.Internal +{ + public class DefaultAuthenticationManagerTests + { + + [Fact] + public async Task AuthenticateWithNoAuthMiddlewareThrows() + { + var context = CreateContext(); + await Assert.ThrowsAsync(async () => await context.Authentication.AuthenticateAsync("Foo")); + } + + [Theory] + [InlineData("Automatic")] + [InlineData("Foo")] + public async Task ChallengeWithNoAuthMiddlewareMayThrow(string scheme) + { + var context = CreateContext(); + await Assert.ThrowsAsync(() => context.Authentication.ChallengeAsync(scheme)); + } + + [Fact] + public async Task SignInWithNoAuthMiddlewareThrows() + { + var context = CreateContext(); + await Assert.ThrowsAsync(() => context.Authentication.SignInAsync("Foo", new ClaimsPrincipal())); + } + + [Fact] + public async Task SignOutWithNoAuthMiddlewareMayThrow() + { + var context = CreateContext(); + await Assert.ThrowsAsync(() => context.Authentication.SignOutAsync("Foo")); + } + + [Fact] + public async Task SignInOutIn() + { + var context = CreateContext(); + var handler = new AuthHandler(); + context.Features.Set(new HttpAuthenticationFeature() { Handler = handler }); + var user = new ClaimsPrincipal(); + await context.Authentication.SignInAsync("ignored", user); + Assert.True(handler.SignedIn); + await context.Authentication.SignOutAsync("ignored"); + Assert.False(handler.SignedIn); + await context.Authentication.SignInAsync("ignored", user); + Assert.True(handler.SignedIn); + await context.Authentication.SignOutAsync("ignored", new AuthenticationProperties() { RedirectUri = "~/logout" }); + Assert.False(handler.SignedIn); + } + + private class AuthHandler : IAuthenticationHandler + { + public bool SignedIn { get; set; } + + public Task AuthenticateAsync(AuthenticateContext context) + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(ChallengeContext context) + { + throw new NotImplementedException(); + } + + public void GetDescriptions(DescribeSchemesContext context) + { + throw new NotImplementedException(); + } + + public Task SignInAsync(SignInContext context) + { + SignedIn = true; + context.Accept(); + return Task.FromResult(0); + } + + public Task SignOutAsync(SignOutContext context) + { + SignedIn = false; + context.Accept(); + return Task.FromResult(0); + } + } + + private HttpContext CreateContext() + { + var context = new DefaultHttpContext(); + return context; + } + } +} +#pragma warning restore CS0618 // Type or member is obsolete diff --git a/src/Http/Http/test/DefaultHttpContextTests.cs b/src/Http/Http/test/DefaultHttpContextTests.cs new file mode 100644 index 0000000000..33f73cf191 --- /dev/null +++ b/src/Http/Http/test/DefaultHttpContextTests.cs @@ -0,0 +1,352 @@ +// 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.Net.WebSockets; +using System.Reflection; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class DefaultHttpContextTests + { + [Fact] + public void GetOnSessionProperty_ThrowsOnMissingSessionFeature() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act & Assert + var exception = Assert.Throws(() => context.Session); + Assert.Equal("Session has not been configured for this application or request.", exception.Message); + } + + [Fact] + public void GetOnSessionProperty_ReturnsAvailableSession() + { + // Arrange + var context = new DefaultHttpContext(); + var session = new TestSession(); + session.Set("key1", null); + session.Set("key2", null); + var feature = new BlahSessionFeature(); + feature.Session = session; + context.Features.Set(feature); + + // Act & Assert + Assert.Same(session, context.Session); + context.Session.Set("key3", null); + Assert.Equal(3, context.Session.Keys.Count()); + } + + [Fact] + public void AllowsSettingSession_WithoutSettingUpSessionFeature_Upfront() + { + // Arrange + var session = new TestSession(); + var context = new DefaultHttpContext(); + + // Act + context.Session = session; + + // Assert + Assert.Same(session, context.Session); + } + + [Fact] + public void SettingSession_OverridesAvailableSession() + { + // Arrange + var context = new DefaultHttpContext(); + var session = new TestSession(); + session.Set("key1", null); + session.Set("key2", null); + var feature = new BlahSessionFeature(); + feature.Session = session; + context.Features.Set(feature); + + // Act + context.Session = new TestSession(); + + // Assert + Assert.NotSame(session, context.Session); + Assert.Empty(context.Session.Keys); + } + + [Fact] + public void EmptyUserIsNeverNull() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.NotNull(context.User); + Assert.Single(context.User.Identities); + Assert.True(object.ReferenceEquals(context.User, context.User)); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.True(string.IsNullOrEmpty(context.User.Identity.AuthenticationType)); + + context.User = null; + Assert.NotNull(context.User); + Assert.Single(context.User.Identities); + Assert.True(object.ReferenceEquals(context.User, context.User)); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.True(string.IsNullOrEmpty(context.User.Identity.AuthenticationType)); + + context.User = new ClaimsPrincipal(); + Assert.NotNull(context.User); + Assert.Empty(context.User.Identities); + Assert.True(object.ReferenceEquals(context.User, context.User)); + Assert.Null(context.User.Identity); + + context.User = new ClaimsPrincipal(new ClaimsIdentity("SomeAuthType")); + Assert.Equal("SomeAuthType", context.User.Identity.AuthenticationType); + Assert.True(context.User.Identity.IsAuthenticated); + } + + [Fact] + public void GetItems_DefaultCollectionProvided() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.Features.Get()); + var items = context.Items; + Assert.NotNull(context.Features.Get()); + Assert.NotNull(items); + Assert.Same(items, context.Items); + var item = new object(); + context.Items["foo"] = item; + Assert.Same(item, context.Items["foo"]); + } + + [Fact] + public void GetItems_DefaultRequestIdentifierAvailable() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.Features.Get()); + var traceIdentifier = context.TraceIdentifier; + Assert.NotNull(context.Features.Get()); + Assert.NotNull(traceIdentifier); + Assert.Same(traceIdentifier, context.TraceIdentifier); + + context.TraceIdentifier = "Hello"; + Assert.Same("Hello", context.TraceIdentifier); + } + + [Fact] + public void SetItems_NewCollectionUsed() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.Features.Get()); + var items = new Dictionary(); + context.Items = items; + Assert.NotNull(context.Features.Get()); + Assert.Same(items, context.Items); + var item = new object(); + items["foo"] = item; + Assert.Same(item, context.Items["foo"]); + } + + [Fact] + public void UpdateFeatures_ClearsCachedFeatures() + { + var features = new FeatureCollection(); + features.Set(new HttpRequestFeature()); + features.Set(new HttpResponseFeature()); + features.Set(new TestHttpWebSocketFeature()); + + // featurecollection is set. all cached interfaces are null. + var context = new DefaultHttpContext(features); + TestAllCachedFeaturesAreNull(context, features); + Assert.Equal(3, features.Count()); + + // getting feature properties populates feature collection with defaults + TestAllCachedFeaturesAreSet(context, features); + Assert.NotEqual(3, features.Count()); + + // featurecollection is null. and all cached interfaces are null. + // only top level is tested because child objects are inaccessible. + context.Uninitialize(); + TestCachedFeaturesAreNull(context, null); + + + var newFeatures = new FeatureCollection(); + newFeatures.Set(new HttpRequestFeature()); + newFeatures.Set(new HttpResponseFeature()); + newFeatures.Set(new TestHttpWebSocketFeature()); + + // featurecollection is set to newFeatures. all cached interfaces are null. + context.Initialize(newFeatures); + TestAllCachedFeaturesAreNull(context, newFeatures); + Assert.Equal(3, newFeatures.Count()); + + // getting feature properties populates new feature collection with defaults + TestAllCachedFeaturesAreSet(context, newFeatures); + Assert.NotEqual(3, newFeatures.Count()); + } + + void TestAllCachedFeaturesAreNull(HttpContext context, IFeatureCollection features) + { + TestCachedFeaturesAreNull(context, features); + TestCachedFeaturesAreNull(context.Request, features); + TestCachedFeaturesAreNull(context.Response, features); +#pragma warning disable CS0618 // Type or member is obsolete + TestCachedFeaturesAreNull(context.Authentication, features); +#pragma warning restore CS0618 // Type or member is obsolete + TestCachedFeaturesAreNull(context.Connection, features); + TestCachedFeaturesAreNull(context.WebSockets, features); + } + + void TestCachedFeaturesAreNull(object value, IFeatureCollection features) + { + var type = value.GetType(); + + var field = type + .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(f => + f.FieldType.GetTypeInfo().IsGenericType && + f.FieldType.GetGenericTypeDefinition() == typeof(FeatureReferences<>)); + + var boxedExpectedStruct = features == null ? + Activator.CreateInstance(field.FieldType) : + Activator.CreateInstance(field.FieldType, features); + + var boxedActualStruct = field.GetValue(value); + + Assert.Equal(boxedExpectedStruct, boxedActualStruct); + } + + void TestAllCachedFeaturesAreSet(HttpContext context, IFeatureCollection features) + { + TestCachedFeaturesAreSet(context, features); + TestCachedFeaturesAreSet(context.Request, features); + TestCachedFeaturesAreSet(context.Response, features); +#pragma warning disable CS0618 // Type or member is obsolete + TestCachedFeaturesAreSet(context.Authentication, features); +#pragma warning restore CS0618 // Type or member is obsolete + TestCachedFeaturesAreSet(context.Connection, features); + TestCachedFeaturesAreSet(context.WebSockets, features); + } + + void TestCachedFeaturesAreSet(object value, IFeatureCollection features) + { + var type = value.GetType(); + + var properties = type + .GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.PropertyType.GetTypeInfo().IsInterface); + + TestFeatureProperties(value, features, properties); + + var fields = type + .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(f => f.FieldType.GetTypeInfo().IsInterface); + + foreach (var field in fields) + { + if (field.FieldType == typeof(IFeatureCollection)) + { + Assert.Same(features, field.GetValue(value)); + } + else + { + var v = field.GetValue(value); + Assert.Same(features[field.FieldType], v); + Assert.NotNull(v); + } + } + + } + + private static void TestFeatureProperties(object value, IFeatureCollection features, IEnumerable properties) + { + foreach (var property in properties) + { + if (property.PropertyType == typeof(IFeatureCollection)) + { + Assert.Same(features, property.GetValue(value)); + } + else + { + if (property.Name.Contains("Feature")) + { + var v = property.GetValue(value); + Assert.Same(features[property.PropertyType], v); + Assert.NotNull(v); + } + } + } + } + + private HttpContext CreateContext() + { + var context = new DefaultHttpContext(); + return context; + } + + private class TestSession : ISession + { + private Dictionary _store + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public string Id { get; set; } + + public bool IsAvailable { get; } = true; + + public IEnumerable Keys { get { return _store.Keys; } } + + public void Clear() + { + _store.Clear(); + } + + public Task CommitAsync(CancellationToken cancellationToken) + { + return Task.FromResult(0); + } + + public Task LoadAsync(CancellationToken cancellationToken) + { + return Task.FromResult(0); + } + + public void Remove(string key) + { + _store.Remove(key); + } + + public void Set(string key, byte[] value) + { + _store[key] = value; + } + + public bool TryGetValue(string key, out byte[] value) + { + return _store.TryGetValue(key, out value); + } + } + + private class BlahSessionFeature : ISessionFeature + { + public ISession Session { get; set; } + } + + private class TestHttpWebSocketFeature : IHttpWebSocketFeature + { + public bool IsWebSocketRequest + { + get + { + throw new NotImplementedException(); + } + } + + public Task AcceptAsync(WebSocketAcceptContext context) + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/Features/FakeResponseFeature.cs b/src/Http/Http/test/Features/FakeResponseFeature.cs new file mode 100644 index 0000000000..43a7acab58 --- /dev/null +++ b/src/Http/Http/test/Features/FakeResponseFeature.cs @@ -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.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FakeResponseFeature : HttpResponseFeature + { + List, object>> _onCompletedCallbacks = new List, object>>(); + + public override void OnCompleted(Func callback, object state) + { + _onCompletedCallbacks.Add(new Tuple, object>(callback, state)); + } + + public async Task CompleteAsync() + { + var callbacks = _onCompletedCallbacks; + _onCompletedCallbacks = null; + foreach (var callback in callbacks) + { + await callback.Item1(callback.Item2); + } + } + } +} diff --git a/src/Http/Http/test/Features/FormFeatureTests.cs b/src/Http/Http/test/Features/FormFeatureTests.cs new file mode 100644 index 0000000000..591f46a43e --- /dev/null +++ b/src/Http/Http/test/Features/FormFeatureTests.cs @@ -0,0 +1,521 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class FormFeatureTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_SimpleData_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2"); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.Equal("bar", formCollection["foo"]); + Assert.Equal("2", formCollection["baz"]); + Assert.Equal(bufferRequest, context.Request.Body.CanSeek); + if (bufferRequest) + { + Assert.Equal(0, context.Request.Body.Position); + } + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + + // Cleanup + await responseFeature.CompleteAsync(); + } + + private const string MultipartContentType = "multipart/form-data; boundary=WebKitFormBoundary5pDRpGheQXaM8k3T"; + + private const string MultipartContentTypeWithSpecialCharacters = "multipart/form-data; boundary=\"WebKitFormBoundary/:5pDRpGheQXaM8k3T\""; + + private const string EmptyMultipartForm = "--WebKitFormBoundary5pDRpGheQXaM8k3T--"; + + // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. + private const string MultipartFormEnd = "--WebKitFormBoundary5pDRpGheQXaM8k3T--\r\n"; + + private const string MultipartFormEndWithSpecialCharacters = "--WebKitFormBoundary/:5pDRpGheQXaM8k3T--\r\n"; + + private const string MultipartFormField = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"description\"\r\n" + +"\r\n" + +"Foo\r\n"; + + private const string MultipartFormFile = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"myfile1\"; filename=\"temp.html\"\r\n" + +"Content-Type: text/html\r\n" + +"\r\n" + +"Hello World\r\n"; + + private const string MultipartFormEncodedFilename = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"myfile1\"; filename=\"temp.html\"; filename*=utf-8\'\'t%c3%a9mp.html\r\n" + +"Content-Type: text/html\r\n" + +"\r\n" + +"Hello World\r\n"; + + private const string MultipartFormFileSpecialCharacters = "--WebKitFormBoundary/:5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"description\"\r\n" + +"\r\n" + +"Foo\r\n"; + + + private const string MultipartFormWithField = + MultipartFormField + + MultipartFormEnd; + + private const string MultipartFormWithFile = + MultipartFormFile + + MultipartFormEnd; + + private const string MultipartFormWithFieldAndFile = + MultipartFormField + + MultipartFormFile + + MultipartFormEnd; + + private const string MultipartFormWithEncodedFilename = + MultipartFormEncodedFilename + + MultipartFormEnd; + + private const string MultipartFormWithSpecialCharacters = + MultipartFormFileSpecialCharacters + + MultipartFormEndWithSpecialCharacters; + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadForm_EmptyMultipart_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(EmptyMultipartForm); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = context.Request.Form; + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); + + // Content + Assert.Equal(0, formCollection.Count); + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); + + // Cleanup + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadForm_MultipartWithField_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithField); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = context.Request.Form; + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); + + // Cleanup + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFile_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFile); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(0, formCollection.Count); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("myfile1", file.Name); + Assert.Equal("temp.html", file.FileName); + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) + { + Assert.True(body.CanSeek); + var content = reader.ReadToEnd(); + Assert.Equal("Hello World", content); + } + + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFileAndQuotedBoundaryString_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithSpecialCharacters); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentTypeWithSpecialCharacters; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = context.Request.Form; + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); + + // Cleanup + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithEncodedFilename_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithEncodedFilename); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(0, formCollection.Count); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("myfile1", file.Name); + Assert.Equal("t\u00e9mp.html", file.FileName); + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""; filename*=utf-8''t%c3%a9mp.html", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) + { + Assert.True(body.CanSeek); + var content = reader.ReadToEnd(); + Assert.Equal("Hello World", content); + } + + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFieldAndFile_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFieldAndFile); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) + { + Assert.True(body.CanSeek); + var content = reader.ReadToEnd(); + Assert.Equal("Hello World", content); + } + + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceeded_Throw(bool bufferRequest) + { + var formContent = new List(); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormEnd)); + + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent.ToArray()); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest, ValueCountLimit = 2 }); + context.Features.Set(formFeature); + + var exception = await Assert.ThrowsAsync (() => context.Request.ReadFormAsync()); + Assert.Equal("Form value count limit 2 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceededWithFiles_Throw(bool bufferRequest) + { + var formContent = new List(); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormEnd)); + + + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent.ToArray()); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest, ValueCountLimit = 2 }); + context.Features.Set(formFeature); + + var exception = await Assert.ThrowsAsync (() => context.Request.ReadFormAsync()); + Assert.Equal("Form value count limit 2 exceeded.", exception.Message); + } + + [Theory] + // FileBufferingReadStream transitions to disk storage after 30kb, and stops pooling buffers at 1mb. + [InlineData(true, 1024)] + [InlineData(false, 1024)] + [InlineData(true, 40 * 1024)] + [InlineData(false, 40 * 1024)] + [InlineData(true, 4 * 1024 * 1024)] + [InlineData(false, 4 * 1024 * 1024)] + public async Task ReadFormAsync_MultipartWithFieldAndMediumFile_ReturnsParsedFormCollection(bool bufferRequest, int fileSize) + { + var fileContents = CreateFile(fileSize); + var formContent = CreateMultipartWithFormAndFile(fileContents); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + using (var body = file.OpenReadStream()) + { + Assert.True(body.CanSeek); + CompareStreams(fileContents, body); + } + + await responseFeature.CompleteAsync(); + } + + private Stream CreateFile(int size) + { + var stream = new MemoryStream(size); + var bytes = Encoding.ASCII.GetBytes("HelloWorld_ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz,0123456789;"); + int written = 0; + while (written < size) + { + var toWrite = Math.Min(size - written, bytes.Length); + stream.Write(bytes, 0, toWrite); + written += toWrite; + } + stream.Position = 0; + return stream; + } + + private Stream CreateMultipartWithFormAndFile(Stream fileContents) + { + var stream = new MemoryStream(); + var header = +MultipartFormField + +"--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + +"Content-Disposition: form-data; name=\"myfile1\"; filename=\"temp.html\"\r\n" + +"Content-Type: text/html\r\n" + +"\r\n"; + var footer = +"\r\n--WebKitFormBoundary5pDRpGheQXaM8k3T--"; + + var bytes = Encoding.ASCII.GetBytes(header); + stream.Write(bytes, 0, bytes.Length); + + fileContents.CopyTo(stream); + fileContents.Position = 0; + + bytes = Encoding.ASCII.GetBytes(footer); + stream.Write(bytes, 0, bytes.Length); + stream.Position = 0; + return stream; + } + + private void CompareStreams(Stream streamA, Stream streamB) + { + Assert.Equal(streamA.Length, streamB.Length); + byte[] bytesA = new byte[1024], bytesB = new byte[1024]; + var readA = streamA.Read(bytesA, 0, bytesA.Length); + var readB = streamB.Read(bytesB, 0, bytesB.Length); + Assert.Equal(readA, readB); + var loops = 0; + while (readA > 0) + { + for (int i = 0; i < readA; i++) + { + if (bytesA[i] != bytesB[i]) + { + throw new Exception($"Value mismatch at loop {loops}, index {i}; A:{bytesA[i]}, B:{bytesB[i]}"); + } + } + + readA = streamA.Read(bytesA, 0, bytesA.Length); + readB = streamB.Read(bytesB, 0, bytesB.Length); + Assert.Equal(readA, readB); + loops++; + } + } + } +} diff --git a/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs b/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs new file mode 100644 index 0000000000..7b17028cdf --- /dev/null +++ b/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs @@ -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 Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class HttpRequestIdentifierFeatureTests + { + [Fact] + public void TraceIdentifier_ReturnsId() + { + var feature = new HttpRequestIdentifierFeature(); + + var id = feature.TraceIdentifier; + + Assert.NotNull(id); + } + + [Fact] + public void TraceIdentifier_ReturnsStableId() + { + var feature = new HttpRequestIdentifierFeature(); + + var id1 = feature.TraceIdentifier; + var id2 = feature.TraceIdentifier; + + Assert.Equal(id1, id2); + } + + [Fact] + public void TraceIdentifier_ReturnsUniqueIdForDifferentInstances() + { + var feature1 = new HttpRequestIdentifierFeature(); + var feature2 = new HttpRequestIdentifierFeature(); + + var id1 = feature1.TraceIdentifier; + var id2 = feature2.TraceIdentifier; + + Assert.NotEqual(id1, id2); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/Features/NonSeekableReadStream.cs b/src/Http/Http/test/Features/NonSeekableReadStream.cs new file mode 100644 index 0000000000..279da7992b --- /dev/null +++ b/src/Http/Http/test/Features/NonSeekableReadStream.cs @@ -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 System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class NonSeekableReadStream : Stream + { + private Stream _inner; + + public NonSeekableReadStream(byte[] data) + : this(new MemoryStream(data)) + { + } + + public NonSeekableReadStream(Stream inner) + { + _inner = inner; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + } +} diff --git a/src/Http/Http/test/Features/QueryFeatureTests.cs b/src/Http/Http/test/Features/QueryFeatureTests.cs new file mode 100644 index 0000000000..e43e3ce7a9 --- /dev/null +++ b/src/Http/Http/test/Features/QueryFeatureTests.cs @@ -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 Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class QueryFeatureTests + { + [Fact] + public void QueryReturnsParsedQueryCollection() + { + // Arrange + var features = new FeatureCollection(); + var request = new HttpRequestFeature(); + request.QueryString = "foo=bar"; + features[typeof(IHttpRequestFeature)] = request; + + var provider = new QueryFeature(features); + + // Act + var queryCollection = provider.Query; + + // Assert + Assert.Equal("bar", queryCollection["foo"]); + } + + [Theory] + [InlineData("?q", "q")] + [InlineData("?q&", "q")] + [InlineData("?q1=abc&q2", "q2")] + [InlineData("?q=", "q")] + [InlineData("?q=&", "q")] + public void KeyWithoutValuesAddedToQueryCollection(string queryString, string emptyParam) + { + var features = new FeatureCollection(); + var request = new HttpRequestFeature(); + request.QueryString = queryString; + features[typeof(IHttpRequestFeature)] = request; + + var provider = new QueryFeature(features); + + var queryCollection = provider.Query; + + Assert.True(queryCollection.Keys.Contains(emptyParam)); + Assert.Equal(string.Empty, queryCollection[emptyParam]); + } + + [Theory] + [InlineData("?&&")] + [InlineData("?&")] + [InlineData("&&")] + public void EmptyKeysNotAddedToQueryCollection(string queryString) + { + var features = new FeatureCollection(); + var request = new HttpRequestFeature(); + request.QueryString = queryString; + features[typeof(IHttpRequestFeature)] = request; + + var provider = new QueryFeature(features); + + var queryCollection = provider.Query; + + Assert.Equal(0, queryCollection.Count); + } + } +} diff --git a/src/Http/Http/test/HeaderDictionaryTests.cs b/src/Http/Http/test/HeaderDictionaryTests.cs new file mode 100644 index 0000000000..03d642a018 --- /dev/null +++ b/src/Http/Http/test/HeaderDictionaryTests.cs @@ -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.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HeaderDictionaryTests + { + public static TheoryData HeaderSegmentData => new TheoryData> + { + new[] { "Value1", "Value2", "Value3", "Value4" }, + new[] { "Value1", "", "Value3", "Value4" }, + new[] { "Value1", "", "", "Value4" }, + new[] { "Value1", "", null, "Value4" }, + new[] { "", "", "", "" }, + new[] { "", null, "", null }, + }; + + [Fact] + public void PropertiesAreAccessible() + { + var headers = new HeaderDictionary( + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Header1", "Value1" } + }); + + Assert.Single(headers); + Assert.Equal(new[] { "Header1" }, headers.Keys); + Assert.True(headers.ContainsKey("header1")); + Assert.False(headers.ContainsKey("header2")); + Assert.Equal("Value1", headers["header1"]); + Assert.Equal(new[] { "Value1" }, headers["header1"].ToArray()); + } + + [Theory] + [MemberData(nameof(HeaderSegmentData))] + public void EmptyHeaderSegmentsAreIgnored(IEnumerable segments) + { + var header = string.Join(",", segments); + + var headers = new HeaderDictionary( + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Header1", header}, + }); + + var result = headers.GetCommaSeparatedValues("Header1"); + var expectedResult = segments.Where(s => !string.IsNullOrEmpty(s)); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void EmtpyQuotedHeaderSegmentsAreIgnored() + { + var headers = new HeaderDictionary( + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Header1", "Value1,\"\",,Value2" }, + }); + + var result = headers.GetCommaSeparatedValues("Header1"); + Assert.Equal(new[] { "Value1", "Value2" }, result); + } + + [Fact] + public void ReadActionsWorkWhenReadOnly() + { + var headers = new HeaderDictionary( + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Header1", "Value1" } + }); + + headers.IsReadOnly = true; + + Assert.Single(headers); + Assert.Equal(new[] { "Header1" }, headers.Keys); + Assert.True(headers.ContainsKey("header1")); + Assert.False(headers.ContainsKey("header2")); + Assert.Equal("Value1", headers["header1"]); + Assert.Equal(new[] { "Value1" }, headers["header1"].ToArray()); + } + + [Fact] + public void WriteActionsThrowWhenReadOnly() + { + var headers = new HeaderDictionary(); + headers.IsReadOnly = true; + + Assert.Throws(() => headers["header1"] = "value1"); + Assert.Throws(() => ((IDictionary)headers)["header1"] = "value1"); + Assert.Throws(() => headers.ContentLength = 12); + Assert.Throws(() => headers.Add(new KeyValuePair("header1", "value1"))); + Assert.Throws(() => headers.Add("header1", "value1")); + Assert.Throws(() => headers.Clear()); + Assert.Throws(() => headers.Remove(new KeyValuePair("header1", "value1"))); + Assert.Throws(() => headers.Remove("header1")); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/HttpContextFactoryTests.cs b/src/Http/Http/test/HttpContextFactoryTests.cs new file mode 100644 index 0000000000..ba983198e7 --- /dev/null +++ b/src/Http/Http/test/HttpContextFactoryTests.cs @@ -0,0 +1,39 @@ +// 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.IO; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HttpContextFactoryTests + { + [Fact] + public void CreateHttpContextSetsHttpContextAccessor() + { + // Arrange + var accessor = new HttpContextAccessor(); + var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()), accessor); + + // Act + var context = contextFactory.Create(new FeatureCollection()); + + // Assert + Assert.True(ReferenceEquals(context, accessor.HttpContext)); + } + + [Fact] + public void AllowsCreatingContextWithoutSettingAccessor() + { + // Arrange + var contextFactory = new HttpContextFactory(Options.Create(new FormOptions())); + + // Act & Assert + var context = contextFactory.Create(new FeatureCollection()); + contextFactory.Dispose(context); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs b/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..a317e99346 --- /dev/null +++ b/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs @@ -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 Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class HttpServiceCollectionExtensionsTests + { + [Fact] + public void AddHttpContextAccessor_AddsWithCorrectLifetime() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddHttpContextAccessor(); + + // Assert + var descriptor = services[0]; + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + Assert.Equal(typeof(HttpContextAccessor), descriptor.ImplementationType); + } + + [Fact] + public void AddHttpContextAccessor_ThrowsWithoutServices() + { + Assert.Throws("services", () => HttpServiceCollectionExtensions.AddHttpContextAccessor(null)); + } + } +} diff --git a/src/Http/Http/test/Internal/ApplicationBuilderTests.cs b/src/Http/Http/test/Internal/ApplicationBuilderTests.cs new file mode 100644 index 0000000000..e1336c82ba --- /dev/null +++ b/src/Http/Http/test/Internal/ApplicationBuilderTests.cs @@ -0,0 +1,35 @@ +// 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; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Internal +{ + public class ApplicationBuilderTests + { + [Fact] + public void BuildReturnsCallableDelegate() + { + var builder = new ApplicationBuilder(null); + var app = builder.Build(); + + var httpContext = new DefaultHttpContext(); + + app.Invoke(httpContext); + Assert.Equal(404, httpContext.Response.StatusCode); + } + + [Fact] + public void PropertiesDictionaryIsDistinctAfterNew() + { + var builder1 = new ApplicationBuilder(null); + builder1.Properties["test"] = "value1"; + + var builder2 = builder1.New(); + builder2.Properties["test"] = "value2"; + + Assert.Equal("value1", builder1.Properties["test"]); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/Internal/BindingAddressTests.cs b/src/Http/Http/test/Internal/BindingAddressTests.cs new file mode 100644 index 0000000000..3bca310e82 --- /dev/null +++ b/src/Http/Http/test/Internal/BindingAddressTests.cs @@ -0,0 +1,70 @@ +// 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.Text; +using Xunit; +namespace Microsoft.AspNetCore.Http.Internal.Tests +{ + public class BindingAddressTests + { + [Theory] + [InlineData("")] + [InlineData("5000")] + [InlineData("//noscheme")] + public void FromUriThrowsForUrlsWithoutSchemeDelimiter(string url) + { + Assert.Throws(() => BindingAddress.Parse(url)); + } + + [Theory] + [InlineData("://")] + [InlineData("://:5000")] + [InlineData("http://")] + [InlineData("http://:5000")] + [InlineData("http:///")] + [InlineData("http:///:5000")] + [InlineData("http:////")] + [InlineData("http:////:5000")] + public void FromUriThrowsForUrlsWithoutHost(string url) + { + Assert.Throws(() => BindingAddress.Parse(url)); + } + + [Theory] + [InlineData("://emptyscheme", "", "emptyscheme", 0, "", "://emptyscheme:0")] + [InlineData("http://+", "http", "+", 80, "", "http://+:80")] + [InlineData("http://*", "http", "*", 80, "", "http://*:80")] + [InlineData("http://localhost", "http", "localhost", 80, "", "http://localhost:80")] + [InlineData("http://www.example.com", "http", "www.example.com", 80, "", "http://www.example.com:80")] + [InlineData("https://www.example.com", "https", "www.example.com", 443, "", "https://www.example.com:443")] + [InlineData("http://www.example.com/", "http", "www.example.com", 80, "", "http://www.example.com:80")] + [InlineData("http://www.example.com/foo?bar=baz", "http", "www.example.com", 80, "/foo?bar=baz", "http://www.example.com:80/foo?bar=baz")] + [InlineData("http://www.example.com:5000", "http", "www.example.com", 5000, "", null)] + [InlineData("https://www.example.com:5000", "https", "www.example.com", 5000, "", null)] + [InlineData("http://www.example.com:5000/", "http", "www.example.com", 5000, "", "http://www.example.com:5000")] + [InlineData("http://www.example.com:NOTAPORT", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] + [InlineData("https://www.example.com:NOTAPORT", "https", "www.example.com:NOTAPORT", 443, "", "https://www.example.com:notaport:443")] + [InlineData("http://www.example.com:NOTAPORT/", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] + [InlineData("http://foo:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "foo:", 80, "/tmp/kestrel-test.sock:5000/doesn't/matter", "http://foo::80/tmp/kestrel-test.sock:5000/doesn't/matter")] + [InlineData("http://unix:foo/tmp/kestrel-test.sock", "http", "unix:foo", 80, "/tmp/kestrel-test.sock", "http://unix:foo:80/tmp/kestrel-test.sock")] + [InlineData("http://unix:5000/tmp/kestrel-test.sock", "http", "unix", 5000, "/tmp/kestrel-test.sock", "http://unix:5000/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock", "http", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("https://unix:/tmp/kestrel-test.sock", "https", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("http://unix:/tmp/kestrel-test.sock:", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:/", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "unix:/tmp/kestrel-test.sock", 0, "5000/doesn't/matter", "http://unix:/tmp/kestrel-test.sock")] + public void UrlsAreParsedCorrectly(string url, string scheme, string host, int port, string pathBase, string toString) + { + var serverAddress = BindingAddress.Parse(url); + + Assert.Equal(scheme, serverAddress.Scheme); + Assert.Equal(host, serverAddress.Host); + Assert.Equal(port, serverAddress.Port); + Assert.Equal(pathBase, serverAddress.PathBase); + + Assert.Equal(toString ?? url, serverAddress.ToString()); + } + } +} diff --git a/src/Http/Http/test/Internal/BufferingHelperTests.cs b/src/Http/Http/test/Internal/BufferingHelperTests.cs new file mode 100644 index 0000000000..9ad48986f5 --- /dev/null +++ b/src/Http/Http/test/Internal/BufferingHelperTests.cs @@ -0,0 +1,19 @@ +// 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.IO; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class BufferingHelperTests + { + [Fact] + public void GetTempDirectory_Returns_Valid_Location() + { + var tempDirectory = BufferingHelper.TempDirectory; + Assert.NotNull(tempDirectory); + Assert.True(Directory.Exists(tempDirectory)); + } + } +} \ No newline at end of file diff --git a/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs new file mode 100644 index 0000000000..dbe1d54dd0 --- /dev/null +++ b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs @@ -0,0 +1,235 @@ +// 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; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultHttpRequestTests + { + [Theory] + [InlineData(0)] + [InlineData(9001)] + [InlineData(65535)] + public void GetContentLength_ReturnsParsedHeader(long value) + { + // Arrange + var request = GetRequestWithContentLength(value.ToString(CultureInfo.InvariantCulture)); + + // Act and Assert + Assert.Equal(value, request.ContentLength); + } + + [Fact] + public void GetContentLength_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var request = GetRequestWithContentLength(contentLength: null); + + // Act and Assert + Assert.Null(request.ContentLength); + } + + [Theory] + [InlineData("cant-parse-this")] + [InlineData("-1000")] + [InlineData("1000.00")] + [InlineData("100/5")] + public void GetContentLength_ReturnsNullIfHeaderCannotBeParsed(string contentLength) + { + // Arrange + var request = GetRequestWithContentLength(contentLength); + + // Act and Assert + Assert.Null(request.ContentLength); + } + + [Fact] + public void GetContentType_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var request = GetRequestWithContentType(contentType: null); + + // Act and Assert + Assert.Null(request.ContentType); + } + + [Fact] + public void Host_GetsHostFromHeaders() + { + // Arrange + const string expected = "localhost:9001"; + + var headers = new HeaderDictionary() + { + { "Host", expected }, + }; + + var request = CreateRequest(headers); + + // Act + var host = request.Host; + + // Assert + Assert.Equal(expected, host.Value); + } + + [Fact] + public void Host_DecodesPunyCode() + { + // Arrange + const string expected = "löcalhöst"; + + var headers = new HeaderDictionary() + { + { "Host", "xn--lcalhst-90ae" }, + }; + + var request = CreateRequest(headers); + + // Act + var host = request.Host; + + // Assert + Assert.Equal(expected, host.Value); + } + + [Fact] + public void Host_EncodesPunyCode() + { + // Arrange + const string expected = "xn--lcalhst-90ae"; + + var headers = new HeaderDictionary(); + + var request = CreateRequest(headers); + + // Act + request.Host = new HostString("löcalhöst"); + + // Assert + Assert.Equal(expected, headers["Host"][0]); + } + + [Fact] + public void IsHttps_CorrectlyReflectsScheme() + { + var request = new DefaultHttpContext().Request; + Assert.Equal(string.Empty, request.Scheme); + Assert.False(request.IsHttps); + request.IsHttps = true; + Assert.Equal("https", request.Scheme); + request.IsHttps = false; + Assert.Equal("http", request.Scheme); + request.Scheme = "ftp"; + Assert.False(request.IsHttps); + request.Scheme = "HTTPS"; + Assert.True(request.IsHttps); + } + + [Fact] + public void Query_GetAndSet() + { + var request = new DefaultHttpContext().Request; + var requestFeature = request.HttpContext.Features.Get(); + Assert.Equal(string.Empty, requestFeature.QueryString); + Assert.Equal(QueryString.Empty, request.QueryString); + var query0 = request.Query; + Assert.NotNull(query0); + Assert.Equal(0, query0.Count); + + requestFeature.QueryString = "?name0=value0&name1=value1"; + var query1 = request.Query; + Assert.NotSame(query0, query1); + Assert.Equal(2, query1.Count); + Assert.Equal("value0", query1["name0"]); + Assert.Equal("value1", query1["name1"]); + + var query2 = new QueryCollection( new Dictionary() + { + { "name2", "value2" } + }); + + request.Query = query2; + Assert.Same(query2, request.Query); + Assert.Equal("?name2=value2", requestFeature.QueryString); + Assert.Equal(new QueryString("?name2=value2"), request.QueryString); + } + + [Fact] + public void Cookies_GetAndSet() + { + var request = new DefaultHttpContext().Request; + var cookieHeaders = request.Headers["Cookie"]; + Assert.Empty(cookieHeaders); + var cookies0 = request.Cookies; + Assert.Empty(cookies0); + Assert.Null(cookies0["key0"]); + Assert.False(cookies0.ContainsKey("key0")); + + var newCookies = new[] { "name0=value0%2C", "%5Ename1=value1" }; + request.Headers["Cookie"] = newCookies; + + cookies0 = RequestCookieCollection.Parse(newCookies); + var cookies1 = request.Cookies; + Assert.Equal(cookies0, cookies1); + Assert.Equal(2, cookies1.Count); + Assert.Equal("value0,", cookies1["name0"]); + Assert.Equal("value1", cookies1["^name1"]); + Assert.Equal(newCookies, request.Headers["Cookie"]); + + var cookies2 = new RequestCookieCollection(new Dictionary() + { + { "name2", "value2" } + }); + request.Cookies = cookies2; + Assert.Equal(cookies2, request.Cookies); + Assert.Equal("value2", request.Cookies["name2"]); + cookieHeaders = request.Headers["Cookie"]; + Assert.Equal(new[] { "name2=value2" }, cookieHeaders); + } + + private static HttpRequest CreateRequest(IHeaderDictionary headers) + { + var context = new DefaultHttpContext(); + context.Features.Get().Headers = headers; + return context.Request; + } + + private static HttpRequest GetRequestWithContentLength(string contentLength = null) + { + return GetRequestWithHeader("Content-Length", contentLength); + } + + private static HttpRequest GetRequestWithContentType(string contentType = null) + { + return GetRequestWithHeader("Content-Type", contentType); + } + + private static HttpRequest GetRequestWithAcceptHeader(string acceptHeader = null) + { + return GetRequestWithHeader("Accept", acceptHeader); + } + + private static HttpRequest GetRequestWithAcceptCharsetHeader(string acceptCharset = null) + { + return GetRequestWithHeader("Accept-Charset", acceptCharset); + } + + private static HttpRequest GetRequestWithHeader(string headerName, string headerValue) + { + var headers = new HeaderDictionary(); + if (headerValue != null) + { + headers.Add(headerName, headerValue); + } + + return CreateRequest(headers); + } + } +} diff --git a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs new file mode 100644 index 0000000000..4764c44a63 --- /dev/null +++ b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs @@ -0,0 +1,90 @@ +// 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; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class DefaultHttpResponseTests + { + [Theory] + [InlineData(0)] + [InlineData(9001)] + [InlineData(65535)] + public void GetContentLength_ReturnsParsedHeader(long value) + { + // Arrange + var response = GetResponseWithContentLength(value.ToString(CultureInfo.InvariantCulture)); + + // Act and Assert + Assert.Equal(value, response.ContentLength); + } + + [Fact] + public void GetContentLength_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var response = GetResponseWithContentLength(contentLength: null); + + // Act and Assert + Assert.Null(response.ContentLength); + } + + [Theory] + [InlineData("cant-parse-this")] + [InlineData("-1000")] + [InlineData("1000.00")] + [InlineData("100/5")] + public void GetContentLength_ReturnsNullIfHeaderCannotBeParsed(string contentLength) + { + // Arrange + var response = GetResponseWithContentLength(contentLength); + + // Act and Assert + Assert.Null(response.ContentLength); + } + + [Fact] + public void GetContentType_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var response = GetResponseWithContentType(contentType: null); + + // Act and Assert + Assert.Null(response.ContentType); + } + + private static HttpResponse CreateResponse(IHeaderDictionary headers) + { + var context = new DefaultHttpContext(); + context.Features.Get().Headers = headers; + return context.Response; + } + + private static HttpResponse GetResponseWithContentLength(string contentLength = null) + { + return GetResponseWithHeader("Content-Length", contentLength); + } + + private static HttpResponse GetResponseWithContentType(string contentType = null) + { + return GetResponseWithHeader("Content-Type", contentType); + } + + private static HttpResponse GetResponseWithHeader(string headerName, string headerValue) + { + var headers = new HeaderDictionary(); + if (headerValue != null) + { + headers.Add(headerName, headerValue); + } + + return CreateResponse(headers); + } + } +} diff --git a/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj b/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj new file mode 100644 index 0000000000..c072fc6f67 --- /dev/null +++ b/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj @@ -0,0 +1,12 @@ + + + + $(StandardTestTfms) + + + + + + + + diff --git a/src/Http/Http/test/RequestCookiesCollectionTests.cs b/src/Http/Http/test/RequestCookiesCollectionTests.cs new file mode 100644 index 0000000000..70106df027 --- /dev/null +++ b/src/Http/Http/test/RequestCookiesCollectionTests.cs @@ -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.Linq; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class RequestCookiesCollectionTests + { + public static TheoryData UnEscapesKeyValues_Data + { + get + { + // key, value, expected + return new TheoryData + { + { "key=value", "key", "value" }, + { "key%2C=%21value", "key,", "!value" }, + { "ke%23y%2C=val%5Eue", "ke#y,", "val^ue" }, + { "base64=QUI%2BREU%2FRw%3D%3D", "base64", "QUI+REU/Rw==" }, + { "base64=QUI+REU/Rw==", "base64", "QUI+REU/Rw==" }, + }; + } + } + + [Theory] + [MemberData(nameof(UnEscapesKeyValues_Data))] + public void UnEscapesKeyValues( + string input, + string expectedKey, + string expectedValue) + { + var cookies = RequestCookieCollection.Parse(new StringValues(input)); + + Assert.Equal(1, cookies.Count); + Assert.Equal(expectedKey, cookies.Keys.Single()); + Assert.Equal(expectedValue, cookies[expectedKey]); + } + } +} diff --git a/src/Http/Http/test/ResponseCookiesTest.cs b/src/Http/Http/test/ResponseCookiesTest.cs new file mode 100644 index 0000000000..5e5c44f89d --- /dev/null +++ b/src/Http/Http/test/ResponseCookiesTest.cs @@ -0,0 +1,124 @@ +// 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.Internal; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class ResponseCookiesTest + { + [Fact] + public void DeleteCookieShouldSetDefaultPath() + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + var testcookie = "TestCookie"; + + cookies.Delete(testcookie); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testcookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); + } + + [Fact] + public void DeleteCookieWithCookieOptionsShouldKeepPropertiesOfCookieOptions() + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + var testcookie = "TestCookie"; + var time = new DateTimeOffset(2000, 1, 1, 1, 1, 1, 1, TimeSpan.Zero); + var options = new CookieOptions + { + Secure = true, + HttpOnly = true, + Path = "/", + Expires = time, + Domain = "example.com", + SameSite = SameSiteMode.Lax + }; + + cookies.Delete(testcookie, options); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testcookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); + Assert.Contains("secure", cookieHeaderValues[0]); + Assert.Contains("httponly", cookieHeaderValues[0]); + Assert.Contains("samesite", cookieHeaderValues[0]); + } + + [Fact] + public void NoParamsDeleteRemovesCookieCreatedByAdd() + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + var testcookie = "TestCookie"; + + cookies.Append(testcookie, testcookie); + cookies.Delete(testcookie); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testcookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); + } + + [Fact] + public void ProvidesMaxAgeWithCookieOptionsArgumentExpectMaxAgeToBeSet() + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + var cookieOptions = new CookieOptions(); + var maxAgeTime = TimeSpan.FromHours(1); + cookieOptions.MaxAge = TimeSpan.FromHours(1); + var testcookie = "TestCookie"; + + cookies.Append(testcookie, testcookie, cookieOptions); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.Contains($"max-age={maxAgeTime.TotalSeconds.ToString()}", cookieHeaderValues[0]); + } + + public static TheoryData EscapesKeyValuesBeforeSettingCookieData + { + get + { + // key, value, object pool, expected + return new TheoryData + { + { "key", "value", "key=value" }, + { "key,", "!value", "key%2C=%21value" }, + { "ke#y,", "val^ue", "ke%23y%2C=val%5Eue" }, + { "base64", "QUI+REU/Rw==", "base64=QUI%2BREU%2FRw%3D%3D" }, + }; + } + } + + [Theory] + [MemberData(nameof(EscapesKeyValuesBeforeSettingCookieData))] + public void EscapesKeyValuesBeforeSettingCookie( + string key, + string value, + string expected) + { + var headers = new HeaderDictionary(); + var cookies = new ResponseCookies(headers, null); + + cookies.Append(key, value); + + var cookieHeaderValues = headers[HeaderNames.SetCookie]; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(expected, cookieHeaderValues[0]); + } + } +} diff --git a/src/Http/Owin/src/DictionaryStringArrayWrapper.cs b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs new file mode 100644 index 0000000000..c4bb38f386 --- /dev/null +++ b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs @@ -0,0 +1,81 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Owin +{ + internal class DictionaryStringArrayWrapper : IDictionary + { + public DictionaryStringArrayWrapper(IHeaderDictionary inner) + { + Inner = inner; + } + + public readonly IHeaderDictionary Inner; + + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + + private StringValues Convert(string[] item) => item; + + private string[] Convert(StringValues item) => item; + + string[] IDictionary.this[string key] + { + get { return ((IDictionary)Inner)[key]; } + set { Inner[key] = value; } + } + + int ICollection>.Count => Inner.Count; + + bool ICollection>.IsReadOnly => Inner.IsReadOnly; + + ICollection IDictionary.Keys => Inner.Keys; + + ICollection IDictionary.Values => Inner.Values.Select(Convert).ToList(); + + void ICollection>.Add(KeyValuePair item) => Inner.Add(Convert(item)); + + void IDictionary.Add(string key, string[] value) => Inner.Add(key, value); + + void ICollection>.Clear() => Inner.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => Inner.Contains(Convert(item)); + + bool IDictionary.ContainsKey(string key) => Inner.ContainsKey(key); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach(var kv in Inner) + { + array[arrayIndex++] = Convert(kv); + } + } + + IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); + + bool IDictionary.Remove(string key) => Inner.Remove(key); + + bool IDictionary.TryGetValue(string key, out string[] value) + { + StringValues temp; + if (Inner.TryGetValue(key, out temp)) + { + value = temp; + return true; + } + value = default(StringValues); + return false; + } + } +} diff --git a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs new file mode 100644 index 0000000000..b31c7e9790 --- /dev/null +++ b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs @@ -0,0 +1,126 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Owin +{ + internal class DictionaryStringValuesWrapper : IHeaderDictionary + { + public DictionaryStringValuesWrapper(IDictionary inner) + { + Inner = inner; + } + + public readonly IDictionary Inner; + + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + + private StringValues Convert(string[] item) => item; + + private string[] Convert(StringValues item) => item; + + StringValues IHeaderDictionary.this[string key] + { + get + { + string[] values; + return Inner.TryGetValue(key, out values) ? values : null; + } + set { Inner[key] = value; } + } + + StringValues IDictionary.this[string key] + { + get { return Inner[key]; } + set { Inner[key] = value; } + } + + public long? ContentLength + { + get + { + long value; + + string[] rawValue; + if (!Inner.TryGetValue(HeaderNames.ContentLength, out rawValue)) + { + return null; + } + + if (rawValue.Length == 1 && + !string.IsNullOrEmpty(rawValue[0]) && + HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value)) + { + return value; + } + + return null; + } + set + { + if (value.HasValue) + { + Inner[HeaderNames.ContentLength] = (StringValues)HeaderUtilities.FormatNonNegativeInt64(value.Value); + } + else + { + Inner.Remove(HeaderNames.ContentLength); + } + } + } + + int ICollection>.Count => Inner.Count; + + bool ICollection>.IsReadOnly => Inner.IsReadOnly; + + ICollection IDictionary.Keys => Inner.Keys; + + ICollection IDictionary.Values => Inner.Values.Select(Convert).ToList(); + + void ICollection>.Add(KeyValuePair item) => Inner.Add(Convert(item)); + + void IDictionary.Add(string key, StringValues value) => Inner.Add(key, value); + + void ICollection>.Clear() => Inner.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => Inner.Contains(Convert(item)); + + bool IDictionary.ContainsKey(string key) => Inner.ContainsKey(key); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var kv in Inner) + { + array[arrayIndex++] = Convert(kv); + } + } + + IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + + bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); + + bool IDictionary.Remove(string key) => Inner.Remove(key); + + bool IDictionary.TryGetValue(string key, out StringValues value) + { + string[] temp; + if (Inner.TryGetValue(key, out temp)) + { + value = temp; + return true; + } + value = default(StringValues); + return false; + } + } +} diff --git a/src/Http/Owin/src/IOwinEnvironmentFeature.cs b/src/Http/Owin/src/IOwinEnvironmentFeature.cs new file mode 100644 index 0000000000..8a476b9f38 --- /dev/null +++ b/src/Http/Owin/src/IOwinEnvironmentFeature.cs @@ -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. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Owin +{ + public interface IOwinEnvironmentFeature + { + IDictionary Environment { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/Microsoft.AspNetCore.Owin.csproj b/src/Http/Owin/src/Microsoft.AspNetCore.Owin.csproj new file mode 100644 index 0000000000..cf9574d7f8 --- /dev/null +++ b/src/Http/Owin/src/Microsoft.AspNetCore.Owin.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core component for running OWIN middleware in an ASP.NET Core application, and to run ASP.NET Core middleware in an OWIN application. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;owin + + + + + + + diff --git a/src/Http/Owin/src/OwinConstants.cs b/src/Http/Owin/src/OwinConstants.cs new file mode 100644 index 0000000000..4234b65aa6 --- /dev/null +++ b/src/Http/Owin/src/OwinConstants.cs @@ -0,0 +1,177 @@ +// 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.Owin +{ + internal static class OwinConstants + { + #region OWIN v1.0.0 - 3.2.1. Request Data + + // http://owin.org/spec/spec/owin-1.0.0.html + + public const string RequestScheme = "owin.RequestScheme"; + public const string RequestMethod = "owin.RequestMethod"; + public const string RequestPathBase = "owin.RequestPathBase"; + public const string RequestPath = "owin.RequestPath"; + public const string RequestQueryString = "owin.RequestQueryString"; + public const string RequestProtocol = "owin.RequestProtocol"; + public const string RequestHeaders = "owin.RequestHeaders"; + public const string RequestBody = "owin.RequestBody"; + + #endregion + + #region OWIN v1.0.1 - 3.2.1 Request Data + + // OWIN 1.0.1 http://owin.org/html/owin.html + + public const string RequestId = "owin.RequestId"; + public const string RequestUser = "owin.RequestUser"; + + #endregion + + #region OWIN v1.0.0 - 3.2.2. Response Data + + // http://owin.org/spec/spec/owin-1.0.0.html + + public const string ResponseStatusCode = "owin.ResponseStatusCode"; + public const string ResponseReasonPhrase = "owin.ResponseReasonPhrase"; + public const string ResponseProtocol = "owin.ResponseProtocol"; + public const string ResponseHeaders = "owin.ResponseHeaders"; + public const string ResponseBody = "owin.ResponseBody"; + + #endregion + + #region OWIN v1.0.0 - 3.2.3. Other Data + + // http://owin.org/spec/spec/owin-1.0.0.html + + public const string CallCancelled = "owin.CallCancelled"; + + public const string OwinVersion = "owin.Version"; + + #endregion + + #region OWIN Keys for IAppBuilder.Properties + + internal static class Builder + { + public const string AddSignatureConversion = "builder.AddSignatureConversion"; + public const string DefaultApp = "builder.DefaultApp"; + } + + #endregion + + #region OWIN Key Guidelines and Common Keys - 6. Common keys + + // http://owin.org/spec/spec/CommonKeys.html + + internal static class CommonKeys + { + public const string ClientCertificate = "ssl.ClientCertificate"; + public const string LoadClientCertAsync = "ssl.LoadClientCertAsync"; + public const string RemoteIpAddress = "server.RemoteIpAddress"; + public const string RemotePort = "server.RemotePort"; + public const string LocalIpAddress = "server.LocalIpAddress"; + public const string LocalPort = "server.LocalPort"; + public const string ConnectionId = "server.ConnectionId"; + public const string TraceOutput = "host.TraceOutput"; + public const string Addresses = "host.Addresses"; + public const string AppName = "host.AppName"; + public const string Capabilities = "server.Capabilities"; + public const string OnSendingHeaders = "server.OnSendingHeaders"; + public const string OnAppDisposing = "host.OnAppDisposing"; + public const string Scheme = "scheme"; + public const string Host = "host"; + public const string Port = "port"; + public const string Path = "path"; + } + + #endregion + + #region SendFiles v0.3.0 + + // http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm + + internal static class SendFiles + { + // 3.1. Startup + + public const string Version = "sendfile.Version"; + public const string Support = "sendfile.Support"; + public const string Concurrency = "sendfile.Concurrency"; + + // 3.2. Per Request + + public const string SendAsync = "sendfile.SendAsync"; + } + + #endregion + + #region Opaque v0.3.0 + + // http://owin.org/spec/extensions/owin-OpaqueStream-Extension-v0.3.0.htm + + internal static class OpaqueConstants + { + // 3.1. Startup + + public const string Version = "opaque.Version"; + + // 3.2. Per Request + + public const string Upgrade = "opaque.Upgrade"; + + // 5. Consumption + + public const string Stream = "opaque.Stream"; + // public const string Version = "opaque.Version"; // redundant, declared above + public const string CallCancelled = "opaque.CallCancelled"; + } + + #endregion + + #region WebSocket v0.4.0 + + // http://owin.org/spec/extensions/owin-OpaqueStream-Extension-v0.3.0.htm + + internal static class WebSocket + { + // 3.1. Startup + + public const string Version = "websocket.Version"; + public const string VersionValue = "1.0"; + + // 3.2. Per Request + + public const string Accept = "websocket.Accept"; + public const string AcceptAlt = "websocket.AcceptAlt"; // Non-spec + + // 4. Accept + + public const string SubProtocol = "websocket.SubProtocol"; + + // 5. Consumption + + public const string SendAsync = "websocket.SendAsync"; + public const string ReceiveAsync = "websocket.ReceiveAsync"; + public const string CloseAsync = "websocket.CloseAsync"; + // public const string Version = "websocket.Version"; // redundant, declared above + public const string CallCancelled = "websocket.CallCancelled"; + public const string ClientCloseStatus = "websocket.ClientCloseStatus"; + public const string ClientCloseDescription = "websocket.ClientCloseDescription"; + } + + #endregion + + #region Security v0.1.0 + + internal static class Security + { + // 3.2. Per Request + + public const string User = "server.User"; + } + + #endregion + } +} diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs new file mode 100644 index 0000000000..6c7f3ad66f --- /dev/null +++ b/src/Http/Owin/src/OwinEnvironment.cs @@ -0,0 +1,397 @@ +// 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; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Owin +{ + using SendFileFunc = Func; + using WebSocketAcceptAlt = + Func + < + WebSocketAcceptContext, // WebSocket Accept parameters + Task + >; + + public class OwinEnvironment : IDictionary + { + private HttpContext _context; + private IDictionary _entries; + + public OwinEnvironment(HttpContext context) + { + if (context.Features.Get() == null) + { + throw new ArgumentException("Missing required feature: " + nameof(IHttpRequestFeature) + ".", nameof(context)); + } + if (context.Features.Get() == null) + { + throw new ArgumentException("Missing required feature: " + nameof(IHttpResponseFeature) + ".", nameof(context)); + } + + _context = context; + _entries = new Dictionary() + { + { OwinConstants.RequestProtocol, new FeatureMap(feature => feature.Protocol, () => string.Empty, (feature, value) => feature.Protocol = Convert.ToString(value)) }, + { OwinConstants.RequestScheme, new FeatureMap(feature => feature.Scheme, () => string.Empty, (feature, value) => feature.Scheme = Convert.ToString(value)) }, + { OwinConstants.RequestMethod, new FeatureMap(feature => feature.Method, () => string.Empty, (feature, value) => feature.Method = Convert.ToString(value)) }, + { OwinConstants.RequestPathBase, new FeatureMap(feature => feature.PathBase, () => string.Empty, (feature, value) => feature.PathBase = Convert.ToString(value)) }, + { OwinConstants.RequestPath, new FeatureMap(feature => feature.Path, () => string.Empty, (feature, value) => feature.Path = Convert.ToString(value)) }, + { OwinConstants.RequestQueryString, new FeatureMap(feature => Utilities.RemoveQuestionMark(feature.QueryString), () => string.Empty, + (feature, value) => feature.QueryString = Utilities.AddQuestionMark(Convert.ToString(value))) }, + { OwinConstants.RequestHeaders, new FeatureMap(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, + { OwinConstants.RequestBody, new FeatureMap(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) }, + { OwinConstants.RequestUser, new FeatureMap(feature => feature.User, () => null, (feature, value) => feature.User = (ClaimsPrincipal)value) }, + + { OwinConstants.ResponseStatusCode, new FeatureMap(feature => feature.StatusCode, () => 200, (feature, value) => feature.StatusCode = Convert.ToInt32(value)) }, + { OwinConstants.ResponseReasonPhrase, new FeatureMap(feature => feature.ReasonPhrase, (feature, value) => feature.ReasonPhrase = Convert.ToString(value)) }, + { OwinConstants.ResponseHeaders, new FeatureMap(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, + { OwinConstants.ResponseBody, new FeatureMap(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) }, + { OwinConstants.CommonKeys.OnSendingHeaders, new FeatureMap( + feature => new Action, object>((cb, state) => { + feature.OnStarting(s => + { + cb(s); + return Task.CompletedTask; + }, state); + })) + }, + + { OwinConstants.CommonKeys.ConnectionId, new FeatureMap(feature => feature.ConnectionId, + (feature, value) => feature.ConnectionId = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + + { OwinConstants.CommonKeys.LocalPort, new FeatureMap(feature => feature.LocalPort.ToString(CultureInfo.InvariantCulture), + (feature, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.CommonKeys.RemotePort, new FeatureMap(feature => feature.RemotePort.ToString(CultureInfo.InvariantCulture), + (feature, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + + { OwinConstants.CommonKeys.LocalIpAddress, new FeatureMap(feature => feature.LocalIpAddress.ToString(), + (feature, value) => feature.LocalIpAddress = IPAddress.Parse(Convert.ToString(value))) }, + { OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap(feature => feature.RemoteIpAddress.ToString(), + (feature, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value))) }, + + { OwinConstants.SendFiles.SendAsync, new FeatureMap(feature => new SendFileFunc(feature.SendFileAsync)) }, + + { OwinConstants.Security.User, new FeatureMap(feature => feature.User, + ()=> null, (feature, value) => feature.User = Utilities.MakeClaimsPrincipal((IPrincipal)value), + () => new HttpAuthenticationFeature()) + }, + + { OwinConstants.RequestId, new FeatureMap(feature => feature.TraceIdentifier, + ()=> null, (feature, value) => feature.TraceIdentifier = (string)value, + () => new HttpRequestIdentifierFeature()) + } + }; + + // owin.CallCancelled is required but the feature may not be present. + if (context.Features.Get() != null) + { + _entries[OwinConstants.CallCancelled] = new FeatureMap(feature => feature.RequestAborted); + } + else if (!_context.Items.ContainsKey(OwinConstants.CallCancelled)) + { + _context.Items[OwinConstants.CallCancelled] = CancellationToken.None; + } + + // owin.Version is required. + if (!context.Items.ContainsKey(OwinConstants.OwinVersion)) + { + _context.Items[OwinConstants.OwinVersion] = "1.0"; + } + + if (context.Request.IsHttps) + { + _entries.Add(OwinConstants.CommonKeys.ClientCertificate, new FeatureMap(feature => feature.ClientCertificate, + (feature, value) => feature.ClientCertificate = (X509Certificate2)value)); + _entries.Add(OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap( + feature => new Func(() => feature.GetClientCertificateAsync(CancellationToken.None)))); + } + + if (context.WebSockets.IsWebSocketRequest) + { + _entries.Add(OwinConstants.WebSocket.AcceptAlt, new FeatureMap(feature => new WebSocketAcceptAlt(feature.AcceptAsync))); + } + + _context.Items[typeof(HttpContext).FullName] = _context; // Store for lookup when we transition back out of OWIN + } + + // Public in case there's a new/custom feature interface that needs to be added. + public IDictionary FeatureMaps + { + get { return _entries; } + } + + void IDictionary.Add(string key, object value) + { + if (_entries.ContainsKey(key)) + { + throw new InvalidOperationException("Key already present"); + } + _context.Items.Add(key, value); + } + + bool IDictionary.ContainsKey(string key) + { + object value; + return ((IDictionary)this).TryGetValue(key, out value); + } + + ICollection IDictionary.Keys + { + get + { + object value; + return _entries.Where(pair => pair.Value.TryGet(_context, out value)) + .Select(pair => pair.Key).Concat(_context.Items.Keys.Select(key => Convert.ToString(key))).ToList(); + } + } + + bool IDictionary.Remove(string key) + { + if (_entries.Remove(key)) + { + return true; + } + return _context.Items.Remove(key); + } + + bool IDictionary.TryGetValue(string key, out object value) + { + FeatureMap entry; + if (_entries.TryGetValue(key, out entry) && entry.TryGet(_context, out value)) + { + return true; + } + return _context.Items.TryGetValue(key, out value); + } + + ICollection IDictionary.Values + { + get { throw new NotImplementedException(); } + } + + object IDictionary.this[string key] + { + get + { + FeatureMap entry; + object value; + if (_entries.TryGetValue(key, out entry) && entry.TryGet(_context, out value)) + { + return value; + } + if (_context.Items.TryGetValue(key, out value)) + { + return value; + } + throw new KeyNotFoundException(key); + } + set + { + FeatureMap entry; + if (_entries.TryGetValue(key, out entry)) + { + if (entry.CanSet) + { + entry.Set(_context, value); + } + else + { + _entries.Remove(key); + if (value != null) + { + _context.Items[key] = value; + } + } + } + else + { + if (value == null) + { + _context.Items.Remove(key); + } + else + { + _context.Items[key] = value; + } + } + } + } + + void ICollection>.Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + void ICollection>.Clear() + { + _entries.Clear(); + _context.Items.Clear(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + int ICollection>.Count + { + get { return _entries.Count + _context.Items.Count; } + } + + bool ICollection>.IsReadOnly + { + get { return false; } + } + + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + foreach (var entryPair in _entries) + { + object value; + if (entryPair.Value.TryGet(_context, out value)) + { + yield return new KeyValuePair(entryPair.Key, value); + } + } + foreach (var entryPair in _context.Items) + { + yield return new KeyValuePair(Convert.ToString(entryPair.Key), entryPair.Value); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public class FeatureMap + { + public FeatureMap(Type featureInterface, Func getter) + : this(featureInterface, getter, defaultFactory: null) + { + } + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory) + : this(featureInterface, getter, defaultFactory, setter: null) + { + } + + public FeatureMap(Type featureInterface, Func getter, Action setter) + : this(featureInterface, getter, defaultFactory: null, setter: setter) + { + } + + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter) + : this(featureInterface, getter, defaultFactory, setter, featureFactory: null) + { + } + + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter, Func featureFactory) + { + FeatureInterface = featureInterface; + Getter = getter; + Setter = setter; + DefaultFactory = defaultFactory; + FeatureFactory = featureFactory; + } + + private Type FeatureInterface { get; set; } + private Func Getter { get; set; } + private Action Setter { get; set; } + private Func DefaultFactory { get; set; } + private Func FeatureFactory { get; set; } + + public bool CanSet + { + get { return Setter != null; } + } + + internal bool TryGet(HttpContext context, out object value) + { + object featureInstance = context.Features[FeatureInterface]; + if (featureInstance == null) + { + value = null; + return false; + } + value = Getter(featureInstance); + if (value == null && DefaultFactory != null) + { + value = DefaultFactory(); + } + return true; + } + + internal void Set(HttpContext context, object value) + { + var feature = context.Features[FeatureInterface]; + if (feature == null) + { + if (FeatureFactory == null) + { + throw new InvalidOperationException("Missing feature: " + FeatureInterface.FullName); // TODO: LOC + } + else + { + feature = FeatureFactory(); + context.Features[FeatureInterface] = feature; + } + } + Setter(feature, value); + } + } + + public class FeatureMap : FeatureMap + { + public FeatureMap(Func getter) + : base(typeof(TFeature), feature => getter((TFeature)feature)) + { + } + + public FeatureMap(Func getter, Func defaultFactory) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory) + { + } + + public FeatureMap(Func getter, Action setter) + : base(typeof(TFeature), feature => getter((TFeature)feature), (feature, value) => setter((TFeature)feature, value)) + { + } + + public FeatureMap(Func getter, Func defaultFactory, Action setter) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value)) + { + } + + public FeatureMap(Func getter, Func defaultFactory, Action setter, Func featureFactory) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value), () => featureFactory()) + { + } + } + } +} diff --git a/src/Http/Owin/src/OwinEnvironmentFeature.cs b/src/Http/Owin/src/OwinEnvironmentFeature.cs new file mode 100644 index 0000000000..14eb312608 --- /dev/null +++ b/src/Http/Owin/src/OwinEnvironmentFeature.cs @@ -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. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Owin +{ + public class OwinEnvironmentFeature : IOwinEnvironmentFeature + { + public IDictionary Environment { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/OwinExtensions.cs b/src/Http/Owin/src/OwinExtensions.cs new file mode 100644 index 0000000000..0344c1a552 --- /dev/null +++ b/src/Http/Owin/src/OwinExtensions.cs @@ -0,0 +1,175 @@ +// 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.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Owin; + +namespace Microsoft.AspNetCore.Builder +{ + using AddMiddleware = Action, Task>, + Func, Task> + >>; + using AppFunc = Func, Task>; + using CreateMiddleware = Func< + Func, Task>, + Func, Task> + >; + + public static class OwinExtensions + { + public static AddMiddleware UseOwin(this IApplicationBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + AddMiddleware add = middleware => + { + Func middleware1 = next1 => + { + AppFunc exitMiddlware = env => + { + return next1((HttpContext)env[typeof(HttpContext).FullName]); + }; + var app = middleware(exitMiddlware); + return httpContext => + { + // Use the existing OWIN env if there is one. + IDictionary env; + var owinEnvFeature = httpContext.Features.Get(); + if (owinEnvFeature != null) + { + env = owinEnvFeature.Environment; + env[typeof(HttpContext).FullName] = httpContext; + } + else + { + env = new OwinEnvironment(httpContext); + } + return app.Invoke(env); + }; + }; + builder.Use(middleware1); + }; + // Adapt WebSockets by default. + add(WebSocketAcceptAdapter.AdaptWebSockets); + return add; + } + + public static IApplicationBuilder UseOwin(this IApplicationBuilder builder, Action pipeline) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + if (pipeline == null) + { + throw new ArgumentNullException(nameof(pipeline)); + } + + pipeline(builder.UseOwin()); + return builder; + } + + public static IApplicationBuilder UseBuilder(this AddMiddleware app) + { + return app.UseBuilder(serviceProvider: null); + } + + public static IApplicationBuilder UseBuilder(this AddMiddleware app, IServiceProvider serviceProvider) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + // Do not set ApplicationBuilder.ApplicationServices to null. May fail later due to missing services but + // at least that results in a more useful Exception than a NRE. + if (serviceProvider == null) + { + serviceProvider = new EmptyProvider(); + } + + // Adapt WebSockets by default. + app(OwinWebSocketAcceptAdapter.AdaptWebSockets); + var builder = new ApplicationBuilder(serviceProvider: serviceProvider); + + var middleware = CreateMiddlewareFactory(exit => + { + builder.Use(ignored => exit); + return builder.Build(); + }, builder.ApplicationServices); + + app(middleware); + return builder; + } + + private static CreateMiddleware CreateMiddlewareFactory(Func middleware, IServiceProvider services) + { + return next => + { + var app = middleware(httpContext => + { + return next(httpContext.Features.Get().Environment); + }); + + return env => + { + // Use the existing HttpContext if there is one. + HttpContext context; + object obj; + if (env.TryGetValue(typeof(HttpContext).FullName, out obj)) + { + context = (HttpContext)obj; + context.Features.Set(new OwinEnvironmentFeature() { Environment = env }); + } + else + { + context = new DefaultHttpContext( + new FeatureCollection( + new OwinFeatureCollection(env))); + context.RequestServices = services; + } + + return app.Invoke(context); + }; + }; + } + + public static AddMiddleware UseBuilder(this AddMiddleware app, Action pipeline) + { + return app.UseBuilder(pipeline, serviceProvider: null); + } + + public static AddMiddleware UseBuilder(this AddMiddleware app, Action pipeline, IServiceProvider serviceProvider) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + if (pipeline == null) + { + throw new ArgumentNullException(nameof(pipeline)); + } + + var builder = app.UseBuilder(serviceProvider); + pipeline(builder); + return app; + } + + private class EmptyProvider : IServiceProvider + { + public object GetService(Type serviceType) + { + return null; + } + } + } +} diff --git a/src/Http/Owin/src/OwinFeatureCollection.cs b/src/Http/Owin/src/OwinFeatureCollection.cs new file mode 100644 index 0000000000..4838b99f5c --- /dev/null +++ b/src/Http/Owin/src/OwinFeatureCollection.cs @@ -0,0 +1,412 @@ +// 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; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.WebSockets; +using System.Reflection; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Owin +{ + using SendFileFunc = Func; + + public class OwinFeatureCollection : + IFeatureCollection, + IHttpRequestFeature, + IHttpResponseFeature, + IHttpConnectionFeature, + IHttpSendFileFeature, + ITlsConnectionFeature, + IHttpRequestIdentifierFeature, + IHttpRequestLifetimeFeature, + IHttpAuthenticationFeature, + IHttpWebSocketFeature, + IOwinEnvironmentFeature + { + public IDictionary Environment { get; set; } + private bool _headersSent; + + public OwinFeatureCollection(IDictionary environment) + { + Environment = environment; + SupportsWebSockets = true; + + var register = Prop, object>>(OwinConstants.CommonKeys.OnSendingHeaders); + register?.Invoke(state => + { + var collection = (OwinFeatureCollection)state; + collection._headersSent = true; + }, this); + } + + T Prop(string key) + { + object value; + if (Environment.TryGetValue(key, out value) && value is T) + { + return (T)value; + } + return default(T); + } + + void Prop(string key, object value) + { + Environment[key] = value; + } + + string IHttpRequestFeature.Protocol + { + get { return Prop(OwinConstants.RequestProtocol); } + set { Prop(OwinConstants.RequestProtocol, value); } + } + + string IHttpRequestFeature.Scheme + { + get { return Prop(OwinConstants.RequestScheme); } + set { Prop(OwinConstants.RequestScheme, value); } + } + + string IHttpRequestFeature.Method + { + get { return Prop(OwinConstants.RequestMethod); } + set { Prop(OwinConstants.RequestMethod, value); } + } + + string IHttpRequestFeature.PathBase + { + get { return Prop(OwinConstants.RequestPathBase); } + set { Prop(OwinConstants.RequestPathBase, value); } + } + + string IHttpRequestFeature.Path + { + get { return Prop(OwinConstants.RequestPath); } + set { Prop(OwinConstants.RequestPath, value); } + } + + string IHttpRequestFeature.QueryString + { + get { return Utilities.AddQuestionMark(Prop(OwinConstants.RequestQueryString)); } + set { Prop(OwinConstants.RequestQueryString, Utilities.RemoveQuestionMark(value)); } + } + + string IHttpRequestFeature.RawTarget + { + get { return string.Empty; } + set { throw new NotSupportedException(); } + } + + IHeaderDictionary IHttpRequestFeature.Headers + { + get { return Utilities.MakeHeaderDictionary(Prop>(OwinConstants.RequestHeaders)); } + set { Prop(OwinConstants.RequestHeaders, Utilities.MakeDictionaryStringArray(value)); } + } + + string IHttpRequestIdentifierFeature.TraceIdentifier + { + get { return Prop(OwinConstants.RequestId); } + set { Prop(OwinConstants.RequestId, value); } + } + + Stream IHttpRequestFeature.Body + { + get { return Prop(OwinConstants.RequestBody); } + set { Prop(OwinConstants.RequestBody, value); } + } + + int IHttpResponseFeature.StatusCode + { + get { return Prop(OwinConstants.ResponseStatusCode); } + set { Prop(OwinConstants.ResponseStatusCode, value); } + } + + string IHttpResponseFeature.ReasonPhrase + { + get { return Prop(OwinConstants.ResponseReasonPhrase); } + set { Prop(OwinConstants.ResponseReasonPhrase, value); } + } + + IHeaderDictionary IHttpResponseFeature.Headers + { + get { return Utilities.MakeHeaderDictionary(Prop>(OwinConstants.ResponseHeaders)); } + set { Prop(OwinConstants.ResponseHeaders, Utilities.MakeDictionaryStringArray(value)); } + } + + Stream IHttpResponseFeature.Body + { + get { return Prop(OwinConstants.ResponseBody); } + set { Prop(OwinConstants.ResponseBody, value); } + } + + bool IHttpResponseFeature.HasStarted + { + get { return _headersSent; } + } + + void IHttpResponseFeature.OnStarting(Func callback, object state) + { + var register = Prop, object>>(OwinConstants.CommonKeys.OnSendingHeaders); + if (register == null) + { + throw new NotSupportedException(OwinConstants.CommonKeys.OnSendingHeaders); + } + + // Need to block on the callback since we can't change the OWIN signature to be async + register(s => callback(s).GetAwaiter().GetResult(), state); + } + + void IHttpResponseFeature.OnCompleted(Func callback, object state) + { + throw new NotSupportedException(); + } + + IPAddress IHttpConnectionFeature.RemoteIpAddress + { + get { return IPAddress.Parse(Prop(OwinConstants.CommonKeys.RemoteIpAddress)); } + set { Prop(OwinConstants.CommonKeys.RemoteIpAddress, value.ToString()); } + } + + IPAddress IHttpConnectionFeature.LocalIpAddress + { + get { return IPAddress.Parse(Prop(OwinConstants.CommonKeys.LocalIpAddress)); } + set { Prop(OwinConstants.CommonKeys.LocalIpAddress, value.ToString()); } + } + + int IHttpConnectionFeature.RemotePort + { + get { return int.Parse(Prop(OwinConstants.CommonKeys.RemotePort)); } + set { Prop(OwinConstants.CommonKeys.RemotePort, value.ToString(CultureInfo.InvariantCulture)); } + } + + int IHttpConnectionFeature.LocalPort + { + get { return int.Parse(Prop(OwinConstants.CommonKeys.LocalPort)); } + set { Prop(OwinConstants.CommonKeys.LocalPort, value.ToString(CultureInfo.InvariantCulture)); } + } + + string IHttpConnectionFeature.ConnectionId + { + get { return Prop(OwinConstants.CommonKeys.ConnectionId); } + set { Prop(OwinConstants.CommonKeys.ConnectionId, value); } + } + + private bool SupportsSendFile + { + get + { + object obj; + return Environment.TryGetValue(OwinConstants.SendFiles.SendAsync, out obj) && obj != null; + } + } + + Task IHttpSendFileFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + object obj; + if (Environment.TryGetValue(OwinConstants.SendFiles.SendAsync, out obj)) + { + var func = (SendFileFunc)obj; + return func(path, offset, length, cancellation); + } + throw new NotSupportedException(OwinConstants.SendFiles.SendAsync); + } + + private bool SupportsClientCerts + { + get + { + object obj; + if (string.Equals("https", ((IHttpRequestFeature)this).Scheme, StringComparison.OrdinalIgnoreCase) + && (Environment.TryGetValue(OwinConstants.CommonKeys.LoadClientCertAsync, out obj) + || Environment.TryGetValue(OwinConstants.CommonKeys.ClientCertificate, out obj)) + && obj != null) + { + return true; + } + return false; + } + } + + X509Certificate2 ITlsConnectionFeature.ClientCertificate + { + get { return Prop(OwinConstants.CommonKeys.ClientCertificate); } + set { Prop(OwinConstants.CommonKeys.ClientCertificate, value); } + } + + async Task ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken) + { + var loadAsync = Prop>(OwinConstants.CommonKeys.LoadClientCertAsync); + if (loadAsync != null) + { + await loadAsync(); + } + return Prop(OwinConstants.CommonKeys.ClientCertificate); + } + + CancellationToken IHttpRequestLifetimeFeature.RequestAborted + { + get { return Prop(OwinConstants.CallCancelled); } + set { Prop(OwinConstants.CallCancelled, value); } + } + + void IHttpRequestLifetimeFeature.Abort() + { + throw new NotImplementedException(); + } + + ClaimsPrincipal IHttpAuthenticationFeature.User + { + get + { + return Prop(OwinConstants.RequestUser) + ?? Utilities.MakeClaimsPrincipal(Prop(OwinConstants.Security.User)); + } + set + { + Prop(OwinConstants.RequestUser, value); + Prop(OwinConstants.Security.User, value); + } + } + + IAuthenticationHandler IHttpAuthenticationFeature.Handler { get; set; } + + /// + /// Gets or sets if the underlying server supports WebSockets. This is enabled by default. + /// The value should be consistent across requests. + /// + public bool SupportsWebSockets { get; set; } + + bool IHttpWebSocketFeature.IsWebSocketRequest + { + get + { + object obj; + return Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj); + } + } + + Task IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) + { + object obj; + if (!Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj)) + { + throw new NotSupportedException("WebSockets are not supported"); // TODO: LOC + } + var accept = (Func>)obj; + return accept(context); + } + + // IFeatureCollection + + public int Revision + { + get { return 0; } // Not modifiable + } + + public bool IsReadOnly + { + get { return true; } + } + + public object this[Type key] + { + get { return Get(key); } + set { throw new NotSupportedException(); } + } + + private bool SupportsInterface(Type key) + { + // Does this type implement the requested interface? + if (key.GetTypeInfo().IsAssignableFrom(GetType().GetTypeInfo())) + { + // Check for conditional features + if (key == typeof(IHttpSendFileFeature)) + { + return SupportsSendFile; + } + else if (key == typeof(ITlsConnectionFeature)) + { + return SupportsClientCerts; + } + else if (key == typeof(IHttpWebSocketFeature)) + { + return SupportsWebSockets; + } + + // The rest of the features are always supported. + return true; + } + return false; + } + + public object Get(Type key) + { + if (SupportsInterface(key)) + { + return this; + } + return null; + } + + public void Set(Type key, object value) + { + throw new NotSupportedException(); + } + + public TFeature Get() + { + return (TFeature)this[typeof(TFeature)]; + } + + public void Set(TFeature instance) + { + this[typeof(TFeature)] = instance; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator> GetEnumerator() + { + yield return new KeyValuePair(typeof(IHttpRequestFeature), this); + yield return new KeyValuePair(typeof(IHttpResponseFeature), this); + yield return new KeyValuePair(typeof(IHttpConnectionFeature), this); + yield return new KeyValuePair(typeof(IHttpRequestIdentifierFeature), this); + yield return new KeyValuePair(typeof(IHttpRequestLifetimeFeature), this); + yield return new KeyValuePair(typeof(IHttpAuthenticationFeature), this); + yield return new KeyValuePair(typeof(IOwinEnvironmentFeature), this); + + // Check for conditional features + if (SupportsSendFile) + { + yield return new KeyValuePair(typeof(IHttpSendFileFeature), this); + } + if (SupportsClientCerts) + { + yield return new KeyValuePair(typeof(ITlsConnectionFeature), this); + } + if (SupportsWebSockets) + { + yield return new KeyValuePair(typeof(IHttpWebSocketFeature), this); + } + } + + public void Dispose() + { + } + } +} + diff --git a/src/Http/Owin/src/Utilities.cs b/src/Http/Owin/src/Utilities.cs new file mode 100644 index 0000000000..b65cae78a9 --- /dev/null +++ b/src/Http/Owin/src/Utilities.cs @@ -0,0 +1,69 @@ +// 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.Security.Principal; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Owin +{ + internal static class Utilities + { + internal static string RemoveQuestionMark(string queryString) + { + if (!string.IsNullOrEmpty(queryString)) + { + if (queryString[0] == '?') + { + return queryString.Substring(1); + } + } + return queryString; + } + + internal static string AddQuestionMark(string queryString) + { + if (!string.IsNullOrEmpty(queryString)) + { + return '?' + queryString; + } + return queryString; + } + + internal static ClaimsPrincipal MakeClaimsPrincipal(IPrincipal principal) + { + if (principal == null) + { + return null; + } + if (principal is ClaimsPrincipal) + { + return principal as ClaimsPrincipal; + } + return new ClaimsPrincipal(principal); + } + + internal static IHeaderDictionary MakeHeaderDictionary(IDictionary dictionary) + { + var wrapper = dictionary as DictionaryStringArrayWrapper; + if (wrapper != null) + { + return wrapper.Inner; + } + return new DictionaryStringValuesWrapper(dictionary); + } + + internal static IDictionary MakeDictionaryStringArray(IHeaderDictionary dictionary) + { + var wrapper = dictionary as DictionaryStringValuesWrapper; + if (wrapper != null) + { + return wrapper.Inner; + } + return new DictionaryStringArrayWrapper(dictionary); + } + } +} diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs new file mode 100644 index 0000000000..5fe43dedd2 --- /dev/null +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs @@ -0,0 +1,143 @@ +// 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.Net.WebSockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Owin +{ + using AppFunc = Func, Task>; + using WebSocketAccept = + Action + < + IDictionary, // WebSocket Accept parameters + Func // WebSocketFunc callback + < + IDictionary, // WebSocket environment + Task // Complete + > + >; + using WebSocketAcceptAlt = + Func + < + WebSocketAcceptContext, // WebSocket Accept parameters + Task + >; + + /// + /// This adapts the OWIN WebSocket accept flow to match the ASP.NET Core WebSocket Accept flow. + /// This enables ASP.NET Core components to use WebSockets on OWIN based servers. + /// + public class OwinWebSocketAcceptAdapter + { + private WebSocketAccept _owinWebSocketAccept; + private TaskCompletionSource _requestTcs = new TaskCompletionSource(); + private TaskCompletionSource _acceptTcs = new TaskCompletionSource(); + private TaskCompletionSource _upstreamWentAsync = new TaskCompletionSource(); + private string _subProtocol = null; + + private OwinWebSocketAcceptAdapter(WebSocketAccept owinWebSocketAccept) + { + _owinWebSocketAccept = owinWebSocketAccept; + } + + private Task RequestTask { get { return _requestTcs.Task; } } + private Task UpstreamTask { get; set; } + private TaskCompletionSource UpstreamWentAsyncTcs { get { return _upstreamWentAsync; } } + + private async Task AcceptWebSocketAsync(WebSocketAcceptContext context) + { + IDictionary options = null; + if (context is OwinWebSocketAcceptContext) + { + var acceptContext = context as OwinWebSocketAcceptContext; + options = acceptContext.Options; + _subProtocol = acceptContext.SubProtocol; + } + else if (context?.SubProtocol != null) + { + options = new Dictionary(1) + { + { OwinConstants.WebSocket.SubProtocol, context.SubProtocol } + }; + _subProtocol = context.SubProtocol; + } + + // Accept may have been called synchronously on the original request thread, we might not have a task yet. Go async. + await _upstreamWentAsync.Task; + + _owinWebSocketAccept(options, OwinAcceptCallback); + _requestTcs.TrySetResult(0); // Let the pipeline unwind. + + return await _acceptTcs.Task; + } + + private Task OwinAcceptCallback(IDictionary webSocketContext) + { + _acceptTcs.TrySetResult(new OwinWebSocketAdapter(webSocketContext, _subProtocol)); + return UpstreamTask; + } + + // Make sure declined websocket requests complete. This is a no-op for accepted websocket requests. + private void EnsureCompleted(Task task) + { + if (task.IsCanceled) + { + _requestTcs.TrySetCanceled(); + } + else if (task.IsFaulted) + { + _requestTcs.TrySetException(task.Exception); + } + else + { + _requestTcs.TrySetResult(0); + } + } + + // Order of operations: + // 1. A WebSocket handshake request is received by the middleware. + // 2. The middleware inserts an alternate Accept signature into the OWIN environment. + // 3. The middleware invokes Next and stores Next's Task locally. It then returns an alternate Task to the server. + // 4. The OwinFeatureCollection adapts the alternate Accept signature to IHttpWebSocketFeature.AcceptAsync. + // 5. A component later in the pipleline invokes IHttpWebSocketFeature.AcceptAsync (mapped to AcceptWebSocketAsync). + // 6. The middleware calls the OWIN Accept, providing a local callback, and returns an incomplete Task. + // 7. The middleware completes the alternate Task it returned from Invoke, telling the server that the request pipeline has completed. + // 8. The server invokes the middleware's callback, which creats a WebSocket adapter complete's the orriginal Accept Task with it. + // 9. The middleware waits while the application uses the WebSocket, where the end is signaled by the Next's Task completion. + public static AppFunc AdaptWebSockets(AppFunc next) + { + return environment => + { + object accept; + if (environment.TryGetValue(OwinConstants.WebSocket.Accept, out accept) && accept is WebSocketAccept) + { + var adapter = new OwinWebSocketAcceptAdapter((WebSocketAccept)accept); + + environment[OwinConstants.WebSocket.AcceptAlt] = new WebSocketAcceptAlt(adapter.AcceptWebSocketAsync); + + try + { + adapter.UpstreamTask = next(environment); + adapter.UpstreamWentAsyncTcs.TrySetResult(0); + adapter.UpstreamTask.ContinueWith(adapter.EnsureCompleted, TaskContinuationOptions.ExecuteSynchronously); + } + catch (Exception ex) + { + adapter.UpstreamWentAsyncTcs.TrySetException(ex); + throw; + } + + return adapter.RequestTask; + } + else + { + return next(environment); + } + }; + } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs new file mode 100644 index 0000000000..a9fd28edba --- /dev/null +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs @@ -0,0 +1,48 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Owin +{ + public class OwinWebSocketAcceptContext : WebSocketAcceptContext + { + private IDictionary _options; + + public OwinWebSocketAcceptContext() : this(new Dictionary(1)) + { + } + + public OwinWebSocketAcceptContext(IDictionary options) + { + _options = options; + } + + public override string SubProtocol + { + get + { + object obj; + if (_options != null && _options.TryGetValue(OwinConstants.WebSocket.SubProtocol, out obj)) + { + return (string)obj; + } + return null; + } + set + { + if (_options == null) + { + _options = new Dictionary(1); + } + _options[OwinConstants.WebSocket.SubProtocol] = value; + } + } + + public IDictionary Options + { + get { return _options; } + } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs new file mode 100644 index 0000000000..e7eed159ea --- /dev/null +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs @@ -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.Buffers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Net.WebSockets; + +namespace Microsoft.AspNetCore.Owin +{ + // http://owin.org/extensions/owin-WebSocket-Extension-v0.4.0.htm + using WebSocketCloseAsync = + Func; + using WebSocketReceiveAsync = + Func /* data */, + CancellationToken /* cancel */, + Task>>; + using WebSocketSendAsync = + Func /* data */, + int /* messageType */, + bool /* endOfMessage */, + CancellationToken /* cancel */, + Task>; + using RawWebSocketReceiveResult = Tuple; // count + + public class OwinWebSocketAdapter : WebSocket + { + private const int _rentedBufferSize = 1024; + private IDictionary _websocketContext; + private WebSocketSendAsync _sendAsync; + private WebSocketReceiveAsync _receiveAsync; + private WebSocketCloseAsync _closeAsync; + private WebSocketState _state; + private string _subProtocol; + + public OwinWebSocketAdapter(IDictionary websocketContext, string subProtocol) + { + _websocketContext = websocketContext; + _sendAsync = (WebSocketSendAsync)websocketContext[OwinConstants.WebSocket.SendAsync]; + _receiveAsync = (WebSocketReceiveAsync)websocketContext[OwinConstants.WebSocket.ReceiveAsync]; + _closeAsync = (WebSocketCloseAsync)websocketContext[OwinConstants.WebSocket.CloseAsync]; + _state = WebSocketState.Open; + _subProtocol = subProtocol; + } + + public override WebSocketCloseStatus? CloseStatus + { + get + { + object obj; + if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseStatus, out obj)) + { + return (WebSocketCloseStatus)obj; + } + return null; + } + } + + public override string CloseStatusDescription + { + get + { + object obj; + if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseDescription, out obj)) + { + return (string)obj; + } + return null; + } + } + + public override string SubProtocol + { + get + { + return _subProtocol; + } + } + + public override WebSocketState State + { + get + { + return _state; + } + } + + public override async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + var rawResult = await _receiveAsync(buffer, cancellationToken); + var messageType = OpCodeToEnum(rawResult.Item1); + if (messageType == WebSocketMessageType.Close) + { + if (State == WebSocketState.Open) + { + _state = WebSocketState.CloseReceived; + } + else if (State == WebSocketState.CloseSent) + { + _state = WebSocketState.Closed; + } + return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2, CloseStatus, CloseStatusDescription); + } + else + { + return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2); + } + } + + public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + return _sendAsync(buffer, EnumToOpCode(messageType), endOfMessage, cancellationToken); + } + + public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + if (State == WebSocketState.Open || State == WebSocketState.CloseReceived) + { + await CloseOutputAsync(closeStatus, statusDescription, cancellationToken); + } + + var buffer = ArrayPool.Shared.Rent(_rentedBufferSize); + try + { + while (State == WebSocketState.CloseSent) + { + // Drain until close received + await ReceiveAsync(new ArraySegment(buffer), cancellationToken); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + // TODO: Validate state + if (State == WebSocketState.Open) + { + _state = WebSocketState.CloseSent; + } + else if (State == WebSocketState.CloseReceived) + { + _state = WebSocketState.Closed; + } + return _closeAsync((int)closeStatus, statusDescription, cancellationToken); + } + + public override void Abort() + { + _state = WebSocketState.Aborted; + } + + public override void Dispose() + { + _state = WebSocketState.Closed; + } + + private static WebSocketMessageType OpCodeToEnum(int messageType) + { + switch (messageType) + { + case 0x1: + return WebSocketMessageType.Text; + case 0x2: + return WebSocketMessageType.Binary; + case 0x8: + return WebSocketMessageType.Close; + default: + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); + } + } + + private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) + { + switch (webSocketMessageType) + { + case WebSocketMessageType.Text: + return 0x1; + case WebSocketMessageType.Binary: + return 0x2; + case WebSocketMessageType.Close: + return 0x8; + default: + throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs b/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs new file mode 100644 index 0000000000..f1355da4c2 --- /dev/null +++ b/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs @@ -0,0 +1,92 @@ +// 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.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Owin +{ + using AppFunc = Func, Task>; + using WebSocketAccept = + Action + < + IDictionary, // WebSocket Accept parameters + Func // WebSocketFunc callback + < + IDictionary, // WebSocket environment + Task // Complete + > + >; + using WebSocketAcceptAlt = + Func + < + WebSocketAcceptContext, // WebSocket Accept parameters + Task + >; + + /// + /// This adapts the ASP.NET Core WebSocket Accept flow to match the OWIN WebSocket accept flow. + /// This enables OWIN based components to use WebSockets on ASP.NET Core servers. + /// + public class WebSocketAcceptAdapter + { + private IDictionary _env; + private WebSocketAcceptAlt _accept; + private AppFunc _callback; + private IDictionary _options; + + public WebSocketAcceptAdapter(IDictionary env, WebSocketAcceptAlt accept) + { + _env = env; + _accept = accept; + } + + private void AcceptWebSocket(IDictionary options, AppFunc callback) + { + _options = options; + _callback = callback; + _env[OwinConstants.ResponseStatusCode] = 101; + } + + public static AppFunc AdaptWebSockets(AppFunc next) + { + return async environment => + { + object accept; + if (environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out accept) && accept is WebSocketAcceptAlt) + { + var adapter = new WebSocketAcceptAdapter(environment, (WebSocketAcceptAlt)accept); + + environment[OwinConstants.WebSocket.Accept] = new WebSocketAccept(adapter.AcceptWebSocket); + await next(environment); + if ((int)environment[OwinConstants.ResponseStatusCode] == 101 && adapter._callback != null) + { + WebSocketAcceptContext acceptContext = null; + object obj; + if (adapter._options != null && adapter._options.TryGetValue(typeof(WebSocketAcceptContext).FullName, out obj)) + { + acceptContext = obj as WebSocketAcceptContext; + } + else if (adapter._options != null) + { + acceptContext = new OwinWebSocketAcceptContext(adapter._options); + } + + var webSocket = await adapter._accept(acceptContext); + var webSocketAdapter = new WebSocketAdapter(webSocket, (CancellationToken)environment[OwinConstants.CallCancelled]); + await adapter._callback(webSocketAdapter.Environment); + await webSocketAdapter.CleanupAsync(); + } + } + else + { + await next(environment); + } + }; + } + } +} \ No newline at end of file diff --git a/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs b/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs new file mode 100644 index 0000000000..7fad98704c --- /dev/null +++ b/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs @@ -0,0 +1,171 @@ +// 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.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Owin +{ + using WebSocketCloseAsync = + Func; + using WebSocketReceiveAsync = + Func /* data */, + CancellationToken /* cancel */, + Task>>; + using WebSocketReceiveTuple = + Tuple; + using WebSocketSendAsync = + Func /* data */, + int /* messageType */, + bool /* endOfMessage */, + CancellationToken /* cancel */, + Task>; + + public class WebSocketAdapter + { + private readonly WebSocket _webSocket; + private readonly IDictionary _environment; + private readonly CancellationToken _cancellationToken; + + internal WebSocketAdapter(WebSocket webSocket, CancellationToken ct) + { + _webSocket = webSocket; + _cancellationToken = ct; + + _environment = new Dictionary(); + _environment[OwinConstants.WebSocket.SendAsync] = new WebSocketSendAsync(SendAsync); + _environment[OwinConstants.WebSocket.ReceiveAsync] = new WebSocketReceiveAsync(ReceiveAsync); + _environment[OwinConstants.WebSocket.CloseAsync] = new WebSocketCloseAsync(CloseAsync); + _environment[OwinConstants.WebSocket.CallCancelled] = ct; + _environment[OwinConstants.WebSocket.Version] = OwinConstants.WebSocket.VersionValue; + + _environment[typeof(WebSocket).FullName] = webSocket; + } + + internal IDictionary Environment + { + get { return _environment; } + } + + internal Task SendAsync(ArraySegment buffer, int messageType, bool endOfMessage, CancellationToken cancel) + { + // Remap close messages to CloseAsync. System.Net.WebSockets.WebSocket.SendAsync does not allow close messages. + if (messageType == 0x8) + { + return RedirectSendToCloseAsync(buffer, cancel); + } + else if (messageType == 0x9 || messageType == 0xA) + { + // Ping & Pong, not allowed by the underlying APIs, silently discard. + return Task.CompletedTask; + } + + return _webSocket.SendAsync(buffer, OpCodeToEnum(messageType), endOfMessage, cancel); + } + + internal async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancel) + { + WebSocketReceiveResult nativeResult = await _webSocket.ReceiveAsync(buffer, cancel); + + if (nativeResult.MessageType == WebSocketMessageType.Close) + { + _environment[OwinConstants.WebSocket.ClientCloseStatus] = (int)(nativeResult.CloseStatus ?? WebSocketCloseStatus.NormalClosure); + _environment[OwinConstants.WebSocket.ClientCloseDescription] = nativeResult.CloseStatusDescription ?? string.Empty; + } + + return new WebSocketReceiveTuple( + EnumToOpCode(nativeResult.MessageType), + nativeResult.EndOfMessage, + nativeResult.Count); + } + + internal Task CloseAsync(int status, string description, CancellationToken cancel) + { + return _webSocket.CloseOutputAsync((WebSocketCloseStatus)status, description, cancel); + } + + private Task RedirectSendToCloseAsync(ArraySegment buffer, CancellationToken cancel) + { + if (buffer.Array == null || buffer.Count == 0) + { + return CloseAsync(1000, string.Empty, cancel); + } + else if (buffer.Count >= 2) + { + // Unpack the close message. + int statusCode = + (buffer.Array[buffer.Offset] << 8) + | buffer.Array[buffer.Offset + 1]; + string description = Encoding.UTF8.GetString(buffer.Array, buffer.Offset + 2, buffer.Count - 2); + + return CloseAsync(statusCode, description, cancel); + } + else + { + throw new ArgumentOutOfRangeException(nameof(buffer)); + } + } + + internal async Task CleanupAsync() + { + switch (_webSocket.State) + { + case WebSocketState.Closed: // Closed gracefully, no action needed. + case WebSocketState.Aborted: // Closed abortively, no action needed. + break; + case WebSocketState.CloseReceived: + // Echo what the client said, if anything. + await _webSocket.CloseAsync(_webSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + _webSocket.CloseStatusDescription ?? string.Empty, _cancellationToken); + break; + case WebSocketState.Open: + case WebSocketState.CloseSent: // No close received, abort so we don't have to drain the pipe. + _webSocket.Abort(); + break; + default: + throw new NotSupportedException($"Unsupported {nameof(WebSocketState)} value: {_webSocket.State}."); + } + } + + private static WebSocketMessageType OpCodeToEnum(int messageType) + { + switch (messageType) + { + case 0x1: + return WebSocketMessageType.Text; + case 0x2: + return WebSocketMessageType.Binary; + case 0x8: + return WebSocketMessageType.Close; + default: + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); + } + } + + private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) + { + switch (webSocketMessageType) + { + case WebSocketMessageType.Text: + return 0x1; + case WebSocketMessageType.Binary: + return 0x2; + case WebSocketMessageType.Close: + return 0x8; + default: + throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); + } + } + } +} diff --git a/src/Http/Owin/src/baseline.netcore.json b/src/Http/Owin/src/baseline.netcore.json new file mode 100644 index 0000000000..8211307418 --- /dev/null +++ b/src/Http/Owin/src/baseline.netcore.json @@ -0,0 +1,1010 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Owin, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.OwinExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseOwin", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "System.Action, System.Threading.Tasks.Task>, System.Func, System.Threading.Tasks.Task>>>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseOwin", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "pipeline", + "Type": "System.Action, System.Threading.Tasks.Task>, System.Func, System.Threading.Tasks.Task>>>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseBuilder", + "Parameters": [ + { + "Name": "app", + "Type": "System.Action, System.Threading.Tasks.Task>, System.Func, System.Threading.Tasks.Task>>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseBuilder", + "Parameters": [ + { + "Name": "app", + "Type": "System.Action, System.Threading.Tasks.Task>, System.Func, System.Threading.Tasks.Task>>>" + }, + { + "Name": "serviceProvider", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseBuilder", + "Parameters": [ + { + "Name": "app", + "Type": "System.Action, System.Threading.Tasks.Task>, System.Func, System.Threading.Tasks.Task>>>" + }, + { + "Name": "pipeline", + "Type": "System.Action" + } + ], + "ReturnType": "System.Action, System.Threading.Tasks.Task>, System.Func, System.Threading.Tasks.Task>>>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseBuilder", + "Parameters": [ + { + "Name": "app", + "Type": "System.Action, System.Threading.Tasks.Task>, System.Func, System.Threading.Tasks.Task>>>" + }, + { + "Name": "pipeline", + "Type": "System.Action" + }, + { + "Name": "serviceProvider", + "Type": "System.IServiceProvider" + } + ], + "ReturnType": "System.Action, System.Threading.Tasks.Task>, System.Func, System.Threading.Tasks.Task>>>", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Environment", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Environment", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinEnvironment", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IDictionary" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FeatureMaps", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinEnvironmentFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Environment", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Environment", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinFeatureCollection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Microsoft.AspNetCore.Http.Features.IHttpRequestFeature", + "Microsoft.AspNetCore.Http.Features.IHttpResponseFeature", + "Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature", + "Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature", + "Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature", + "Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature", + "Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature", + "Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature", + "Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature", + "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Environment", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Environment", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SupportsWebSockets", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SupportsWebSockets", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Revision", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsReadOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + } + ], + "ReturnType": "System.Object", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Item", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + } + ], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "key", + "Type": "System.Type" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Get", + "Parameters": [], + "ReturnType": "T0", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Set", + "Parameters": [ + { + "Name": "instance", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.IFeatureCollection", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "environment", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinWebSocketAcceptAdapter", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AdaptWebSockets", + "Parameters": [ + { + "Name": "next", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinWebSocketAcceptContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Http.WebSocketAcceptContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SubProtocol", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SubProtocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Options", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinWebSocketAdapter", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Net.WebSockets.WebSocket", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CloseStatus", + "Parameters": [], + "ReturnType": "System.Nullable", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CloseStatusDescription", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SubProtocol", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_State", + "Parameters": [], + "ReturnType": "System.Net.WebSockets.WebSocketState", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReceiveAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.ArraySegment" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SendAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.ArraySegment" + }, + { + "Name": "messageType", + "Type": "System.Net.WebSockets.WebSocketMessageType" + }, + { + "Name": "endOfMessage", + "Type": "System.Boolean" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CloseAsync", + "Parameters": [ + { + "Name": "closeStatus", + "Type": "System.Net.WebSockets.WebSocketCloseStatus" + }, + { + "Name": "statusDescription", + "Type": "System.String" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CloseOutputAsync", + "Parameters": [ + { + "Name": "closeStatus", + "Type": "System.Net.WebSockets.WebSocketCloseStatus" + }, + { + "Name": "statusDescription", + "Type": "System.String" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Abort", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "websocketContext", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "subProtocol", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.WebSocketAcceptAdapter", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AdaptWebSockets", + "Parameters": [ + { + "Name": "next", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "env", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "accept", + "Type": "System.Func>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.WebSocketAdapter", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinEnvironment+FeatureMap", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_CanSet", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func" + }, + { + "Name": "defaultFactory", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func" + }, + { + "Name": "setter", + "Type": "System.Action" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func" + }, + { + "Name": "defaultFactory", + "Type": "System.Func" + }, + { + "Name": "setter", + "Type": "System.Action" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "featureInterface", + "Type": "System.Type" + }, + { + "Name": "getter", + "Type": "System.Func" + }, + { + "Name": "defaultFactory", + "Type": "System.Func" + }, + { + "Name": "setter", + "Type": "System.Action" + }, + { + "Name": "featureFactory", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Owin.OwinEnvironment+FeatureMap", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Owin.OwinEnvironment+FeatureMap", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func" + }, + { + "Name": "defaultFactory", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func" + }, + { + "Name": "setter", + "Type": "System.Action" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func" + }, + { + "Name": "defaultFactory", + "Type": "System.Func" + }, + { + "Name": "setter", + "Type": "System.Action" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "getter", + "Type": "System.Func" + }, + { + "Name": "defaultFactory", + "Type": "System.Func" + }, + { + "Name": "setter", + "Type": "System.Action" + }, + { + "Name": "featureFactory", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TFeature", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Http/Owin/test/Microsoft.AspNetCore.Owin.Tests.csproj b/src/Http/Owin/test/Microsoft.AspNetCore.Owin.Tests.csproj new file mode 100644 index 0000000000..359aff75b9 --- /dev/null +++ b/src/Http/Owin/test/Microsoft.AspNetCore.Owin.Tests.csproj @@ -0,0 +1,13 @@ + + + + $(StandardTestTfms) + + + + + + + + + diff --git a/src/Http/Owin/test/OwinEnvironmentTests.cs b/src/Http/Owin/test/OwinEnvironmentTests.cs new file mode 100644 index 0000000000..b728802914 --- /dev/null +++ b/src/Http/Owin/test/OwinEnvironmentTests.cs @@ -0,0 +1,148 @@ +// 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; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Owin +{ + public class OwinEnvironmentTests + { + private T Get(IDictionary environment, string key) + { + object value; + return environment.TryGetValue(key, out value) ? (T)value : default(T); + } + + [Fact] + public void OwinEnvironmentCanBeCreated() + { + HttpContext context = CreateContext(); + context.Request.Method = "SomeMethod"; + context.User = new ClaimsPrincipal(new ClaimsIdentity("Foo")); + context.Request.Body = Stream.Null; + context.Request.Headers["CustomRequestHeader"] = "CustomRequestValue"; + context.Request.Path = new PathString("/path"); + context.Request.PathBase = new PathString("/pathBase"); + context.Request.Protocol = "http/1.0"; + context.Request.QueryString = new QueryString("?key=value"); + context.Request.Scheme = "http"; + context.Response.Body = Stream.Null; + context.Response.Headers["CustomResponseHeader"] = "CustomResponseValue"; + context.Response.StatusCode = 201; + + IDictionary env = new OwinEnvironment(context); + Assert.Equal("SomeMethod", Get(env, "owin.RequestMethod")); + // User property should set both server.User (non-standard) and owin.RequestUser. + Assert.Equal("Foo", Get(env, "server.User").Identity.AuthenticationType); + Assert.Equal("Foo", Get(env, "owin.RequestUser").Identity.AuthenticationType); + Assert.Same(Stream.Null, Get(env, "owin.RequestBody")); + var requestHeaders = Get>(env, "owin.RequestHeaders"); + Assert.NotNull(requestHeaders); + Assert.Equal("CustomRequestValue", requestHeaders["CustomRequestHeader"].First()); + Assert.Equal("/path", Get(env, "owin.RequestPath")); + Assert.Equal("/pathBase", Get(env, "owin.RequestPathBase")); + Assert.Equal("http/1.0", Get(env, "owin.RequestProtocol")); + Assert.Equal("key=value", Get(env, "owin.RequestQueryString")); + Assert.Equal("http", Get(env, "owin.RequestScheme")); + + Assert.Same(Stream.Null, Get(env, "owin.ResponseBody")); + var responseHeaders = Get>(env, "owin.ResponseHeaders"); + Assert.NotNull(responseHeaders); + Assert.Equal("CustomResponseValue", responseHeaders["CustomResponseHeader"].First()); + Assert.Equal(201, Get(env, "owin.ResponseStatusCode")); + } + + [Fact] + public void OwinEnvironmentCanBeModified() + { + HttpContext context = CreateContext(); + IDictionary env = new OwinEnvironment(context); + + env["owin.RequestMethod"] = "SomeMethod"; + env["server.User"] = new ClaimsPrincipal(new ClaimsIdentity("Foo")); + Assert.Equal("Foo", context.User.Identity.AuthenticationType); + // User property should fall back from owin.RequestUser to server.User. + env["owin.RequestUser"] = new ClaimsPrincipal(new ClaimsIdentity("Bar")); + Assert.Equal("Bar", context.User.Identity.AuthenticationType); + env["owin.RequestBody"] = Stream.Null; + var requestHeaders = Get>(env, "owin.RequestHeaders"); + Assert.NotNull(requestHeaders); + requestHeaders["CustomRequestHeader"] = new[] { "CustomRequestValue" }; + env["owin.RequestPath"] = "/path"; + env["owin.RequestPathBase"] = "/pathBase"; + env["owin.RequestProtocol"] = "http/1.0"; + env["owin.RequestQueryString"] = "key=value"; + env["owin.RequestScheme"] = "http"; + env["owin.ResponseBody"] = Stream.Null; + var responseHeaders = Get>(env, "owin.ResponseHeaders"); + Assert.NotNull(responseHeaders); + responseHeaders["CustomResponseHeader"] = new[] { "CustomResponseValue" }; + env["owin.ResponseStatusCode"] = 201; + + Assert.Equal("SomeMethod", context.Request.Method); + Assert.Same(Stream.Null, context.Request.Body); + Assert.Equal("CustomRequestValue", context.Request.Headers["CustomRequestHeader"]); + Assert.Equal("/path", context.Request.Path.Value); + Assert.Equal("/pathBase", context.Request.PathBase.Value); + Assert.Equal("http/1.0", context.Request.Protocol); + Assert.Equal("?key=value", context.Request.QueryString.Value); + Assert.Equal("http", context.Request.Scheme); + + Assert.Same(Stream.Null, context.Response.Body); + Assert.Equal("CustomResponseValue", context.Response.Headers["CustomResponseHeader"]); + Assert.Equal(201, context.Response.StatusCode); + } + + [Theory] + [InlineData("server.LocalPort")] + public void OwinEnvironmentDoesNotContainEntriesForMissingFeatures(string key) + { + HttpContext context = CreateContext(); + IDictionary env = new OwinEnvironment(context); + + object value; + Assert.False(env.TryGetValue(key, out value)); + + Assert.Throws(() => env[key]); + + Assert.False(env.Keys.Contains(key)); + Assert.False(env.ContainsKey(key)); + } + + [Fact] + public void OwinEnvironmentSuppliesDefaultsForMissingRequiredEntries() + { + HttpContext context = CreateContext(); + IDictionary env = new OwinEnvironment(context); + + object value; + Assert.True(env.TryGetValue("owin.CallCancelled", out value), "owin.CallCancelled"); + Assert.True(env.TryGetValue("owin.Version", out value), "owin.Version"); + + Assert.Equal(CancellationToken.None, env["owin.CallCancelled"]); + Assert.Equal("1.0", env["owin.Version"]); + } + + [Fact] + public void OwinEnvironmentImpelmentsGetEnumerator() + { + var owinEnvironment = new OwinEnvironment(CreateContext()); + + Assert.NotNull(owinEnvironment.GetEnumerator()); + Assert.NotNull(((IEnumerable)owinEnvironment).GetEnumerator()); + } + + private HttpContext CreateContext() + { + var context = new DefaultHttpContext(); + return context; + } + } +} diff --git a/src/Http/Owin/test/OwinExtensionTests.cs b/src/Http/Owin/test/OwinExtensionTests.cs new file mode 100644 index 0000000000..c4c51fba0a --- /dev/null +++ b/src/Http/Owin/test/OwinExtensionTests.cs @@ -0,0 +1,164 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Owin +{ + using AddMiddleware = Action, Task>, + Func, Task> + >>; + using AppFunc = Func, Task>; + using CreateMiddleware = Func< + Func, Task>, + Func, Task> + >; + + public class OwinExtensionTests + { + static AppFunc notFound = env => new Task(() => { env["owin.ResponseStatusCode"] = 404; }); + + [Fact] + public async Task OwinConfigureServiceProviderAddsServices() + { + var list = new List(); + AddMiddleware build = list.Add; + IServiceProvider serviceProvider = null; + FakeService fakeService = null; + + var builder = build.UseBuilder(applicationBuilder => + { + serviceProvider = applicationBuilder.ApplicationServices; + applicationBuilder.Run(context => + { + fakeService = context.RequestServices.GetService(); + return Task.FromResult(0); + }); + }, + new ServiceCollection().AddSingleton(new FakeService()).BuildServiceProvider()); + + list.Reverse(); + await list + .Aggregate(notFound, (next, middleware) => middleware(next)) + .Invoke(new Dictionary()); + + Assert.NotNull(serviceProvider); + Assert.NotNull(serviceProvider.GetService()); + Assert.NotNull(fakeService); + } + + [Fact] + public async Task OwinDefaultNoServices() + { + var list = new List(); + AddMiddleware build = list.Add; + IServiceProvider expectedServiceProvider = new ServiceCollection().BuildServiceProvider(); + IServiceProvider serviceProvider = null; + FakeService fakeService = null; + bool builderExecuted = false; + bool applicationExecuted = false; + + var builder = build.UseBuilder(applicationBuilder => + { + builderExecuted = true; + serviceProvider = applicationBuilder.ApplicationServices; + applicationBuilder.Run(context => + { + applicationExecuted = true; + fakeService = context.RequestServices.GetService(); + return Task.FromResult(0); + }); + }, + expectedServiceProvider); + + list.Reverse(); + await list + .Aggregate(notFound, (next, middleware) => middleware(next)) + .Invoke(new Dictionary()); + + Assert.True(builderExecuted); + Assert.Equal(expectedServiceProvider, serviceProvider); + Assert.True(applicationExecuted); + Assert.Null(fakeService); + } + + [Fact] + public async Task OwinDefaultNullServiceProvider() + { + var list = new List(); + AddMiddleware build = list.Add; + IServiceProvider serviceProvider = null; + FakeService fakeService = null; + bool builderExecuted = false; + bool applicationExecuted = false; + + var builder = build.UseBuilder(applicationBuilder => + { + builderExecuted = true; + serviceProvider = applicationBuilder.ApplicationServices; + applicationBuilder.Run(context => + { + applicationExecuted = true; + fakeService = context.RequestServices.GetService(); + return Task.FromResult(0); + }); + }); + + list.Reverse(); + await list + .Aggregate(notFound, (next, middleware) => middleware(next)) + .Invoke(new Dictionary()); + + Assert.True(builderExecuted); + Assert.NotNull(serviceProvider); + Assert.True(applicationExecuted); + Assert.Null(fakeService); + } + + [Fact] + public async Task UseOwin() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var builder = new ApplicationBuilder(serviceProvider); + IDictionary environment = null; + var context = new DefaultHttpContext(); + + builder.UseOwin(addToPipeline => + { + addToPipeline(next => + { + Assert.NotNull(next); + return async env => + { + environment = env; + await next(env); + }; + }); + }); + await builder.Build().Invoke(context); + + // Dictionary contains context but does not contain "websocket.Accept" or "websocket.AcceptAlt" keys. + Assert.NotNull(environment); + var value = Assert.Single( + environment, + kvp => string.Equals(typeof(HttpContext).FullName, kvp.Key, StringComparison.Ordinal)) + .Value; + Assert.Equal(context, value); + Assert.False(environment.ContainsKey("websocket.Accept")); + Assert.False(environment.ContainsKey("websocket.AcceptAlt")); + } + + private class FakeService + { + } + } +} diff --git a/src/Http/Owin/test/OwinFeatureCollectionTests.cs b/src/Http/Owin/test/OwinFeatureCollectionTests.cs new file mode 100644 index 0000000000..b2755961c8 --- /dev/null +++ b/src/Http/Owin/test/OwinFeatureCollectionTests.cs @@ -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.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Owin +{ + public class OwinHttpEnvironmentTests + { + private T Get(IFeatureCollection features) + { + return (T)features[typeof(T)]; + } + + private T Get(IDictionary env, string key) + { + object value; + return env.TryGetValue(key, out value) ? (T)value : default(T); + } + + [Fact] + public void OwinHttpEnvironmentCanBeCreated() + { + var env = new Dictionary + { + { "owin.RequestMethod", HttpMethods.Post }, + { "owin.RequestPath", "/path" }, + { "owin.RequestPathBase", "/pathBase" }, + { "owin.RequestQueryString", "name=value" }, + }; + var features = new OwinFeatureCollection(env); + + var requestFeature = Get(features); + Assert.Equal(requestFeature.Method, HttpMethods.Post); + Assert.Equal("/path", requestFeature.Path); + Assert.Equal("/pathBase", requestFeature.PathBase); + Assert.Equal("?name=value", requestFeature.QueryString); + } + + [Fact] + public void OwinHttpEnvironmentCanBeModified() + { + var env = new Dictionary + { + { "owin.RequestMethod", HttpMethods.Post }, + { "owin.RequestPath", "/path" }, + { "owin.RequestPathBase", "/pathBase" }, + { "owin.RequestQueryString", "name=value" }, + }; + var features = new OwinFeatureCollection(env); + + var requestFeature = Get(features); + requestFeature.Method = HttpMethods.Get; + requestFeature.Path = "/path2"; + requestFeature.PathBase = "/pathBase2"; + requestFeature.QueryString = "?name=value2"; + + Assert.Equal(HttpMethods.Get, Get(env, "owin.RequestMethod")); + Assert.Equal("/path2", Get(env, "owin.RequestPath")); + Assert.Equal("/pathBase2", Get(env, "owin.RequestPathBase")); + Assert.Equal("name=value2", Get(env, "owin.RequestQueryString")); + } + } +} + diff --git a/src/Http/README.md b/src/Http/README.md new file mode 100644 index 0000000000..58e2500a02 --- /dev/null +++ b/src/Http/README.md @@ -0,0 +1,6 @@ +Http Abstractions +================= + +This folders contains projects for HTTP abstractions for ASP.NET Core such as `HttpContext`, `HttpRequest`, `HttpResponse` and `RequestDelegate`. + +It also contains `IApplicationBuilder` and extensions to create and compose your application's pipeline. diff --git a/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs b/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs new file mode 100644 index 0000000000..304ee6522f --- /dev/null +++ b/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs @@ -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. + +namespace Microsoft.AspNetCore.WebUtilities +{ + public static class Base64UrlTextEncoder + { + /// + /// Encodes supplied data into Base64 and replaces any URL encodable characters into non-URL encodable + /// characters. + /// + /// Data to be encoded. + /// Base64 encoded string modified with non-URL encodable characters + public static string Encode(byte[] data) + { + return WebEncoders.Base64UrlEncode(data); + } + + /// + /// Decodes supplied string by replacing the non-URL encodable characters with URL encodable characters and + /// then decodes the Base64 string. + /// + /// The string to be decoded. + /// The decoded data. + public static byte[] Decode(string text) + { + return WebEncoders.Base64UrlDecode(text); + } + } +} diff --git a/src/Http/WebUtilities/src/BufferedReadStream.cs b/src/Http/WebUtilities/src/BufferedReadStream.cs new file mode 100644 index 0000000000..10f1465f3a --- /dev/null +++ b/src/Http/WebUtilities/src/BufferedReadStream.cs @@ -0,0 +1,431 @@ +// 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.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// + /// A Stream that wraps another stream and allows reading lines. + /// The data is buffered in memory. + /// + public class BufferedReadStream : Stream + { + private const byte CR = (byte)'\r'; + private const byte LF = (byte)'\n'; + + private readonly Stream _inner; + private readonly byte[] _buffer; + private readonly ArrayPool _bytePool; + private int _bufferOffset = 0; + private int _bufferCount = 0; + private bool _disposed; + + /// + /// Creates a new stream. + /// + /// The stream to wrap. + /// Size of buffer in bytes. + public BufferedReadStream(Stream inner, int bufferSize) + : this(inner, bufferSize, ArrayPool.Shared) + { + } + + /// + /// Creates a new stream. + /// + /// The stream to wrap. + /// Size of buffer in bytes. + /// ArrayPool for the buffer. + public BufferedReadStream(Stream inner, int bufferSize, ArrayPool bytePool) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + _inner = inner; + _bytePool = bytePool; + _buffer = bytePool.Rent(bufferSize); + } + + /// + /// The currently buffered data. + /// + public ArraySegment BufferedData + { + get { return new ArraySegment(_buffer, _bufferOffset, _bufferCount); } + } + + /// + public override bool CanRead + { + get { return _inner.CanRead || _bufferCount > 0; } + } + + /// + public override bool CanSeek + { + get { return _inner.CanSeek; } + } + + /// + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + /// + public override bool CanWrite + { + get { return _inner.CanWrite; } + } + + /// + public override long Length + { + get { return _inner.Length; } + } + + /// + public override long Position + { + get { return _inner.Position - _bufferCount; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Position must be positive."); + } + if (value == Position) + { + return; + } + + // Backwards? + if (value <= _inner.Position) + { + // Forward within the buffer? + var innerOffset = (int)(_inner.Position - value); + if (innerOffset <= _bufferCount) + { + // Yes, just skip some of the buffered data + _bufferOffset += innerOffset; + _bufferCount -= innerOffset; + } + else + { + // No, reset the buffer + _bufferOffset = 0; + _bufferCount = 0; + _inner.Position = value; + } + } + else + { + // Forward, reset the buffer + _bufferOffset = 0; + _bufferCount = 0; + _inner.Position = value; + } + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + else // if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + return Position; + } + + /// + public override void SetLength(long value) + { + _inner.SetLength(value); + } + + /// + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + _bytePool.Return(_buffer); + + if (disposing) + { + _inner.Dispose(); + } + } + } + + /// + public override void Flush() + { + _inner.Flush(); + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return _inner.Read(buffer, offset, count); + } + + /// + public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return await _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + /// + /// Ensures that the buffer is not empty. + /// + /// Returns true if the buffer is not empty; false otherwise. + public bool EnsureBuffered() + { + if (_bufferCount > 0) + { + return true; + } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = _inner.Read(_buffer, 0, _buffer.Length); + return _bufferCount > 0; + } + + /// + /// Ensures that the buffer is not empty. + /// + /// Cancellation token. + /// Returns true if the buffer is not empty; false otherwise. + public async Task EnsureBufferedAsync(CancellationToken cancellationToken) + { + if (_bufferCount > 0) + { + return true; + } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken); + return _bufferCount > 0; + } + + /// + /// Ensures that a minimum amount of buffered data is available. + /// + /// Minimum amount of buffered data. + /// Returns true if the minimum amount of buffered data is available; false otherwise. + public bool EnsureBuffered(int minCount) + { + if (minCount > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length.ToString()); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) + { + if (_bufferCount > 0) + { + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); + } + _bufferOffset = 0; + } + int read = _inner.Read(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset); + _bufferCount += read; + if (read == 0) + { + return false; + } + } + return true; + } + + /// + /// Ensures that a minimum amount of buffered data is available. + /// + /// Minimum amount of buffered data. + /// Cancellation token. + /// Returns true if the minimum amount of buffered data is available; false otherwise. + public async Task EnsureBufferedAsync(int minCount, CancellationToken cancellationToken) + { + if (minCount > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length.ToString()); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) + { + if (_bufferCount > 0) + { + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); + } + _bufferOffset = 0; + } + int read = await _inner.ReadAsync(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset, cancellationToken); + _bufferCount += read; + if (read == 0) + { + return false; + } + } + return true; + } + + /// + /// Reads a line. A line is defined as a sequence of characters followed by + /// a carriage return immediately followed by a line feed. The resulting string does not + /// contain the terminating carriage return and line feed. + /// + /// Maximum allowed line length. + /// A line. + public string ReadLine(int lengthLimit) + { + CheckDisposed(); + using (var builder = new MemoryStream(200)) + { + bool foundCR = false, foundCRLF = false; + + while (!foundCRLF && EnsureBuffered()) + { + if (builder.Length > lengthLimit) + { + throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); + } + ProcessLineChar(builder, ref foundCR, ref foundCRLF); + } + + return DecodeLine(builder, foundCRLF); + } + } + + /// + /// Reads a line. A line is defined as a sequence of characters followed by + /// a carriage return immediately followed by a line feed. The resulting string does not + /// contain the terminating carriage return and line feed. + /// + /// Maximum allowed line length. + /// Cancellation token. + /// A line. + public async Task ReadLineAsync(int lengthLimit, CancellationToken cancellationToken) + { + CheckDisposed(); + using (var builder = new MemoryStream(200)) + { + bool foundCR = false, foundCRLF = false; + + while (!foundCRLF && await EnsureBufferedAsync(cancellationToken)) + { + if (builder.Length > lengthLimit) + { + throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); + } + + ProcessLineChar(builder, ref foundCR, ref foundCRLF); + } + + return DecodeLine(builder, foundCRLF); + } + } + + private void ProcessLineChar(MemoryStream builder, ref bool foundCR, ref bool foundCRLF) + { + var b = _buffer[_bufferOffset]; + builder.WriteByte(b); + _bufferOffset++; + _bufferCount--; + if (b == LF && foundCR) + { + foundCRLF = true; + return; + } + foundCR = b == CR; + } + + private string DecodeLine(MemoryStream builder, bool foundCRLF) + { + // Drop the final CRLF, if any + var length = foundCRLF ? builder.Length - 2 : builder.Length; + return Encoding.UTF8.GetString(builder.ToArray(), 0, (int)length); + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(BufferedReadStream)); + } + } + + private void ValidateBuffer(byte[] buffer, int offset, int count) + { + // Delegate most of our validation. + var ignored = new ArraySegment(buffer, offset, count); + if (count == 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "The value must be greater than zero."); + } + } + } +} diff --git a/src/Http/WebUtilities/src/FileBufferingReadStream.cs b/src/Http/WebUtilities/src/FileBufferingReadStream.cs new file mode 100644 index 0000000000..9dd1fbf13f --- /dev/null +++ b/src/Http/WebUtilities/src/FileBufferingReadStream.cs @@ -0,0 +1,354 @@ +// 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.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// + /// A Stream that wraps another stream and enables rewinding by buffering the content as it is read. + /// The content is buffered in memory up to a certain size and then spooled to a temp file on disk. + /// The temp file will be deleted on Dispose. + /// + public class FileBufferingReadStream : Stream + { + private const int _maxRentedBufferSize = 1024 * 1024; // 1MB + private readonly Stream _inner; + private readonly ArrayPool _bytePool; + private readonly int _memoryThreshold; + private readonly long? _bufferLimit; + private string _tempFileDirectory; + private readonly Func _tempFileDirectoryAccessor; + private string _tempFileName; + + private Stream _buffer; + private byte[] _rentedBuffer; + private bool _inMemory = true; + private bool _completelyBuffered; + + private bool _disposed; + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + Func tempFileDirectoryAccessor) + : this(inner, memoryThreshold, bufferLimit, tempFileDirectoryAccessor, ArrayPool.Shared) + { + } + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + Func tempFileDirectoryAccessor, + ArrayPool bytePool) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + if (tempFileDirectoryAccessor == null) + { + throw new ArgumentNullException(nameof(tempFileDirectoryAccessor)); + } + + _bytePool = bytePool; + if (memoryThreshold < _maxRentedBufferSize) + { + _rentedBuffer = bytePool.Rent(memoryThreshold); + _buffer = new MemoryStream(_rentedBuffer); + _buffer.SetLength(0); + } + else + { + _buffer = new MemoryStream(); + } + + _inner = inner; + _memoryThreshold = memoryThreshold; + _bufferLimit = bufferLimit; + _tempFileDirectoryAccessor = tempFileDirectoryAccessor; + } + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + string tempFileDirectory) + : this(inner, memoryThreshold, bufferLimit, tempFileDirectory, ArrayPool.Shared) + { + } + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + string tempFileDirectory, + ArrayPool bytePool) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + if (tempFileDirectory == null) + { + throw new ArgumentNullException(nameof(tempFileDirectory)); + } + + _bytePool = bytePool; + if (memoryThreshold < _maxRentedBufferSize) + { + _rentedBuffer = bytePool.Rent(memoryThreshold); + _buffer = new MemoryStream(_rentedBuffer); + _buffer.SetLength(0); + } + else + { + _buffer = new MemoryStream(); + } + + _inner = inner; + _memoryThreshold = memoryThreshold; + _bufferLimit = bufferLimit; + _tempFileDirectory = tempFileDirectory; + } + + public bool InMemory + { + get { return _inMemory; } + } + + public string TempFileName + { + get { return _tempFileName; } + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _buffer.Length; } + } + + public override long Position + { + get { return _buffer.Position; } + // Note this will not allow seeking forward beyond the end of the buffer. + set + { + ThrowIfDisposed(); + _buffer.Position = value; + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + if (!_completelyBuffered && origin == SeekOrigin.End) + { + // Can't seek from the end until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); + } + else if (!_completelyBuffered && origin == SeekOrigin.Current && offset + Position > Length) + { + // Can't seek past the end of the buffer until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); + } + else if (!_completelyBuffered && origin == SeekOrigin.Begin && offset > Length) + { + // Can't seek past the end of the buffer until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); + } + return _buffer.Seek(offset, origin); + } + + private Stream CreateTempFile() + { + if (_tempFileDirectory == null) + { + Debug.Assert(_tempFileDirectoryAccessor != null); + _tempFileDirectory = _tempFileDirectoryAccessor(); + Debug.Assert(_tempFileDirectory != null); + } + + _tempFileName = Path.Combine(_tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid().ToString() + ".tmp"); + return new FileStream(_tempFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete, 1024 * 16, + FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.SequentialScan); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + if (_buffer.Position < _buffer.Length || _completelyBuffered) + { + // Just read from the buffer + return _buffer.Read(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position)); + } + + int read = _inner.Read(buffer, offset, count); + + if (_bufferLimit.HasValue && _bufferLimit - read < _buffer.Length) + { + Dispose(); + throw new IOException("Buffer limit exceeded."); + } + + if (_inMemory && _buffer.Length + read > _memoryThreshold) + { + _inMemory = false; + var oldBuffer = _buffer; + _buffer = CreateTempFile(); + if (_rentedBuffer == null) + { + oldBuffer.Position = 0; + var rentedBuffer = _bytePool.Rent(Math.Min((int)oldBuffer.Length, _maxRentedBufferSize)); + var copyRead = oldBuffer.Read(rentedBuffer, 0, rentedBuffer.Length); + while (copyRead > 0) + { + _buffer.Write(rentedBuffer, 0, copyRead); + copyRead = oldBuffer.Read(rentedBuffer, 0, rentedBuffer.Length); + } + _bytePool.Return(rentedBuffer); + } + else + { + _buffer.Write(_rentedBuffer, 0, (int)oldBuffer.Length); + _bytePool.Return(_rentedBuffer); + _rentedBuffer = null; + } + } + + if (read > 0) + { + _buffer.Write(buffer, offset, read); + } + else + { + _completelyBuffered = true; + } + + return read; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + if (_buffer.Position < _buffer.Length || _completelyBuffered) + { + // Just read from the buffer + return await _buffer.ReadAsync(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position), cancellationToken); + } + + int read = await _inner.ReadAsync(buffer, offset, count, cancellationToken); + + if (_bufferLimit.HasValue && _bufferLimit - read < _buffer.Length) + { + Dispose(); + throw new IOException("Buffer limit exceeded."); + } + + if (_inMemory && _buffer.Length + read > _memoryThreshold) + { + _inMemory = false; + var oldBuffer = _buffer; + _buffer = CreateTempFile(); + if (_rentedBuffer == null) + { + oldBuffer.Position = 0; + var rentedBuffer = _bytePool.Rent(Math.Min((int)oldBuffer.Length, _maxRentedBufferSize)); + // oldBuffer is a MemoryStream, no need to do async reads. + var copyRead = oldBuffer.Read(rentedBuffer, 0, rentedBuffer.Length); + while (copyRead > 0) + { + await _buffer.WriteAsync(rentedBuffer, 0, copyRead, cancellationToken); + copyRead = oldBuffer.Read(rentedBuffer, 0, rentedBuffer.Length); + } + _bytePool.Return(rentedBuffer); + } + else + { + await _buffer.WriteAsync(_rentedBuffer, 0, (int)oldBuffer.Length, cancellationToken); + _bytePool.Return(_rentedBuffer); + _rentedBuffer = null; + } + } + + if (read > 0) + { + await _buffer.WriteAsync(buffer, offset, read, cancellationToken); + } + else + { + _completelyBuffered = true; + } + + return read; + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + if (_rentedBuffer != null) + { + _bytePool.Return(_rentedBuffer); + } + + if (disposing) + { + _buffer.Dispose(); + } + } + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(FileBufferingReadStream)); + } + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/FileMultipartSection.cs b/src/Http/WebUtilities/src/FileMultipartSection.cs new file mode 100644 index 0000000000..70d7741f64 --- /dev/null +++ b/src/Http/WebUtilities/src/FileMultipartSection.cs @@ -0,0 +1,70 @@ +// 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.IO; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// + /// Represents a file multipart section + /// + public class FileMultipartSection + { + private ContentDispositionHeaderValue _contentDispositionHeader; + + /// + /// Creates a new instance of the class + /// + /// The section from which to create the + /// Reparses the content disposition header + public FileMultipartSection(MultipartSection section) + :this(section, section.GetContentDispositionHeader()) + { + } + + /// + /// Creates a new instance of the class + /// + /// The section from which to create the + /// An already parsed content disposition header + public FileMultipartSection(MultipartSection section, ContentDispositionHeaderValue header) + { + if (!header.IsFileDisposition()) + { + throw new ArgumentException($"Argument must be a file section", nameof(section)); + } + + Section = section; + _contentDispositionHeader = header; + + Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); + FileName = HeaderUtilities.RemoveQuotes( + _contentDispositionHeader.FileNameStar.HasValue ? + _contentDispositionHeader.FileNameStar : + _contentDispositionHeader.FileName).ToString(); + } + + /// + /// Gets the original section from which this object was created + /// + public MultipartSection Section { get; } + + /// + /// Gets the file stream from the section body + /// + public Stream FileStream => Section.Body; + + /// + /// Gets the name of the section + /// + public string Name { get; } + + /// + /// Gets the name of the file from the section + /// + public string FileName { get; } + + } +} diff --git a/src/Http/WebUtilities/src/FormMultipartSection.cs b/src/Http/WebUtilities/src/FormMultipartSection.cs new file mode 100644 index 0000000000..01af0455b8 --- /dev/null +++ b/src/Http/WebUtilities/src/FormMultipartSection.cs @@ -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.Threading.Tasks; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// + /// Represents a form multipart section + /// + public class FormMultipartSection + { + private ContentDispositionHeaderValue _contentDispositionHeader; + + /// + /// Creates a new instance of the class + /// + /// The section from which to create the + /// Reparses the content disposition header + public FormMultipartSection(MultipartSection section) + : this(section, section.GetContentDispositionHeader()) + { + } + + /// + /// Creates a new instance of the class + /// + /// The section from which to create the + /// An already parsed content disposition header + public FormMultipartSection(MultipartSection section, ContentDispositionHeaderValue header) + { + if (header == null || !header.IsFormDisposition()) + { + throw new ArgumentException($"Argument must be a form section", nameof(section)); + } + + Section = section; + _contentDispositionHeader = header; + Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); + } + + /// + /// Gets the original section from which this object was created + /// + public MultipartSection Section { get; } + + /// + /// The form name + /// + public string Name { get; } + + /// + /// Gets the form value + /// + /// The form value + public Task GetValueAsync() + { + return Section.ReadAsStringAsync(); + } + } +} diff --git a/src/Http/WebUtilities/src/FormReader.cs b/src/Http/WebUtilities/src/FormReader.cs new file mode 100644 index 0000000000..958a4971fa --- /dev/null +++ b/src/Http/WebUtilities/src/FormReader.cs @@ -0,0 +1,312 @@ +// 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.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// + /// Used to read an 'application/x-www-form-urlencoded' form. + /// + public class FormReader : IDisposable + { + public const int DefaultValueCountLimit = 1024; + public const int DefaultKeyLengthLimit = 1024 * 2; + public const int DefaultValueLengthLimit = 1024 * 1024 * 4; + + private const int _rentedCharPoolLength = 8192; + private readonly TextReader _reader; + private readonly char[] _buffer; + private readonly ArrayPool _charPool; + private readonly StringBuilder _builder = new StringBuilder(); + private int _bufferOffset; + private int _bufferCount; + private string _currentKey; + private string _currentValue; + private bool _endOfStream; + private bool _disposed; + + public FormReader(string data) + : this(data, ArrayPool.Shared) + { + } + + public FormReader(string data, ArrayPool charPool) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + _buffer = charPool.Rent(_rentedCharPoolLength); + _charPool = charPool; + _reader = new StringReader(data); + } + + public FormReader(Stream stream) + : this(stream, Encoding.UTF8, ArrayPool.Shared) + { + } + + public FormReader(Stream stream, Encoding encoding) + : this(stream, encoding, ArrayPool.Shared) + { + } + + public FormReader(Stream stream, Encoding encoding, ArrayPool charPool) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + _buffer = charPool.Rent(_rentedCharPoolLength); + _charPool = charPool; + _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); + } + + /// + /// The limit on the number of form values to allow in ReadForm or ReadFormAsync. + /// + public int ValueCountLimit { get; set; } = DefaultValueCountLimit; + + /// + /// The limit on the length of form keys. + /// + public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit; + + /// + /// The limit on the length of form values. + /// + public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit; + + // Format: key1=value1&key2=value2 + /// + /// Reads the next key value pair from the form. + /// For unbuffered data use the async overload instead. + /// + /// The next key value pair, or null when the end of the form is reached. + public KeyValuePair? ReadNextPair() + { + ReadNextPairImpl(); + if (ReadSucceded()) + { + return new KeyValuePair(_currentKey, _currentValue); + } + return null; + } + + private void ReadNextPairImpl() + { + StartReadNextPair(); + while (!_endOfStream) + { + // Empty + if (_bufferCount == 0) + { + Buffer(); + } + if (TryReadNextPair()) + { + break; + } + } + } + + // Format: key1=value1&key2=value2 + /// + /// Asynchronously reads the next key value pair from the form. + /// + /// + /// The next key value pair, or null when the end of the form is reached. + public async Task?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken()) + { + await ReadNextPairAsyncImpl(cancellationToken); + if (ReadSucceded()) + { + return new KeyValuePair(_currentKey, _currentValue); + } + return null; + } + + private async Task ReadNextPairAsyncImpl(CancellationToken cancellationToken = new CancellationToken()) + { + StartReadNextPair(); + while (!_endOfStream) + { + // Empty + if (_bufferCount == 0) + { + await BufferAsync(cancellationToken); + } + if (TryReadNextPair()) + { + break; + } + } + } + + private void StartReadNextPair() + { + _currentKey = null; + _currentValue = null; + } + + private bool TryReadNextPair() + { + if (_currentKey == null) + { + if (!TryReadWord('=', KeyLengthLimit, out _currentKey)) + { + return false; + } + + if (_bufferCount == 0) + { + return false; + } + } + + if (_currentValue == null) + { + if (!TryReadWord('&', ValueLengthLimit, out _currentValue)) + { + return false; + } + } + return true; + } + + private bool TryReadWord(char seperator, int limit, out string value) + { + do + { + if (ReadChar(seperator, limit, out value)) + { + return true; + } + } while (_bufferCount > 0); + return false; + } + + private bool ReadChar(char seperator, int limit, out string word) + { + // End + if (_bufferCount == 0) + { + word = BuildWord(); + return true; + } + + var c = _buffer[_bufferOffset++]; + _bufferCount--; + + if (c == seperator) + { + word = BuildWord(); + return true; + } + if (_builder.Length >= limit) + { + throw new InvalidDataException($"Form key or value length limit {limit} exceeded."); + } + _builder.Append(c); + word = null; + return false; + } + + // '+' un-escapes to ' ', %HH un-escapes as ASCII (or utf-8?) + private string BuildWord() + { + _builder.Replace('+', ' '); + var result = _builder.ToString(); + _builder.Clear(); + return Uri.UnescapeDataString(result); // TODO: Replace this, it's not completely accurate. + } + + private void Buffer() + { + _bufferOffset = 0; + _bufferCount = _reader.Read(_buffer, 0, _buffer.Length); + _endOfStream = _bufferCount == 0; + } + + private async Task BufferAsync(CancellationToken cancellationToken) + { + // TODO: StreamReader doesn't support cancellation? + cancellationToken.ThrowIfCancellationRequested(); + _bufferOffset = 0; + _bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length); + _endOfStream = _bufferCount == 0; + } + + /// + /// Parses text from an HTTP form body. + /// + /// The collection containing the parsed HTTP form body. + public Dictionary ReadForm() + { + var accumulator = new KeyValueAccumulator(); + while (!_endOfStream) + { + ReadNextPairImpl(); + Append(ref accumulator); + } + return accumulator.GetResults(); + } + + /// + /// Parses an HTTP form body. + /// + /// The . + /// The collection containing the parsed HTTP form body. + public async Task> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()) + { + var accumulator = new KeyValueAccumulator(); + while (!_endOfStream) + { + await ReadNextPairAsyncImpl(cancellationToken); + Append(ref accumulator); + } + return accumulator.GetResults(); + } + + private bool ReadSucceded() + { + return _currentKey != null && _currentValue != null; + } + + private void Append(ref KeyValueAccumulator accumulator) + { + if (ReadSucceded()) + { + accumulator.Append(_currentKey, _currentValue); + if (accumulator.ValueCount > ValueCountLimit) + { + throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded."); + } + } + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + _charPool.Return(_buffer); + } + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/HttpRequestStreamReader.cs b/src/Http/WebUtilities/src/HttpRequestStreamReader.cs new file mode 100644 index 0000000000..3f9478c5de --- /dev/null +++ b/src/Http/WebUtilities/src/HttpRequestStreamReader.cs @@ -0,0 +1,374 @@ +// 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.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class HttpRequestStreamReader : TextReader + { + private const int DefaultBufferSize = 1024; + private const int MinBufferSize = 128; + private const int MaxSharedBuilderCapacity = 360; // also the max capacity used in StringBuilderCache + + private Stream _stream; + private readonly Encoding _encoding; + private readonly Decoder _decoder; + + private readonly ArrayPool _bytePool; + private readonly ArrayPool _charPool; + + private readonly int _byteBufferSize; + private byte[] _byteBuffer; + private char[] _charBuffer; + + private int _charBufferIndex; + private int _charsRead; + private int _bytesRead; + + private bool _isBlocked; + private bool _disposed; + + public HttpRequestStreamReader(Stream stream, Encoding encoding) + : this(stream, encoding, DefaultBufferSize, ArrayPool.Shared, ArrayPool.Shared) + { + } + + public HttpRequestStreamReader(Stream stream, Encoding encoding, int bufferSize) + : this(stream, encoding, bufferSize, ArrayPool.Shared, ArrayPool.Shared) + { + } + + public HttpRequestStreamReader( + Stream stream, + Encoding encoding, + int bufferSize, + ArrayPool bytePool, + ArrayPool charPool) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); + _bytePool = bytePool ?? throw new ArgumentNullException(nameof(bytePool)); + _charPool = charPool ?? throw new ArgumentNullException(nameof(charPool)); + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + if (!stream.CanRead) + { + throw new ArgumentException(Resources.HttpRequestStreamReader_StreamNotReadable, nameof(stream)); + } + + _byteBufferSize = bufferSize; + + _decoder = encoding.GetDecoder(); + _byteBuffer = _bytePool.Rent(bufferSize); + + try + { + var requiredLength = encoding.GetMaxCharCount(bufferSize); + _charBuffer = _charPool.Rent(requiredLength); + } + catch + { + _bytePool.Return(_byteBuffer); + + if (_charBuffer != null) + { + _charPool.Return(_charBuffer); + } + + throw; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + _bytePool.Return(_byteBuffer); + _charPool.Return(_charBuffer); + } + + base.Dispose(disposing); + } + + public override int Peek() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } + + if (_charBufferIndex == _charsRead) + { + if (_isBlocked || ReadIntoBuffer() == 0) + { + return -1; + } + } + + return _charBuffer[_charBufferIndex]; + } + + public override int Read() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } + + if (_charBufferIndex == _charsRead) + { + if (ReadIntoBuffer() == 0) + { + return -1; + } + } + + return _charBuffer[_charBufferIndex++]; + } + + public override int Read(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0 || index + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } + + var charsRead = 0; + while (count > 0) + { + var charsRemaining = _charsRead - _charBufferIndex; + if (charsRemaining == 0) + { + charsRemaining = ReadIntoBuffer(); + } + + if (charsRemaining == 0) + { + break; // We're at EOF + } + + if (charsRemaining > count) + { + charsRemaining = count; + } + + Buffer.BlockCopy( + _charBuffer, + _charBufferIndex * 2, + buffer, + (index + charsRead) * 2, + charsRemaining * 2); + _charBufferIndex += charsRemaining; + + charsRead += charsRemaining; + count -= charsRemaining; + + // If we got back fewer chars than we asked for, then it's likely the underlying stream is blocked. + // Send the data back to the caller so they can process it. + if (_isBlocked) + { + break; + } + } + + return charsRead; + } + + public override async Task ReadAsync(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0 || index + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } + + if (_charBufferIndex == _charsRead && await ReadIntoBufferAsync() == 0) + { + return 0; + } + + var charsRead = 0; + while (count > 0) + { + // n is the characters available in _charBuffer + var n = _charsRead - _charBufferIndex; + + // charBuffer is empty, let's read from the stream + if (n == 0) + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; + + // We loop here so that we read in enough bytes to yield at least 1 char. + // We break out of the loop if the stream is blocked (EOF is reached). + do + { + Debug.Assert(n == 0); + _bytesRead = await _stream.ReadAsync( + _byteBuffer, + 0, + _byteBufferSize); + if (_bytesRead == 0) // EOF + { + _isBlocked = true; + break; + } + + // _isBlocked == whether we read fewer bytes than we asked for. + _isBlocked = (_bytesRead < _byteBufferSize); + + Debug.Assert(n == 0); + + _charBufferIndex = 0; + n = _decoder.GetChars( + _byteBuffer, + 0, + _bytesRead, + _charBuffer, + 0); + + Debug.Assert(n > 0); + + _charsRead += n; // Number of chars in StreamReader's buffer. + } + while (n == 0); + + if (n == 0) + { + break; // We're at EOF + } + } + + // Got more chars in charBuffer than the user requested + if (n > count) + { + n = count; + } + + Buffer.BlockCopy( + _charBuffer, + _charBufferIndex * 2, + buffer, + (index + charsRead) * 2, + n * 2); + + _charBufferIndex += n; + + charsRead += n; + count -= n; + + // This function shouldn't block for an indefinite amount of time, + // or reading from a network stream won't work right. If we got + // fewer bytes than we requested, then we want to break right here. + if (_isBlocked) + { + break; + } + } + + return charsRead; + } + + private int ReadIntoBuffer() + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; + + do + { + _bytesRead = _stream.Read(_byteBuffer, 0, _byteBufferSize); + if (_bytesRead == 0) // We're at EOF + { + return _charsRead; + } + + _isBlocked = (_bytesRead < _byteBufferSize); + _charsRead += _decoder.GetChars( + _byteBuffer, + 0, + _bytesRead, + _charBuffer, + _charsRead); + } + while (_charsRead == 0); + + return _charsRead; + } + + private async Task ReadIntoBufferAsync() + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; + + do + { + + _bytesRead = await _stream.ReadAsync( + _byteBuffer, + 0, + _byteBufferSize).ConfigureAwait(false); + if (_bytesRead == 0) + { + // We're at EOF + return _charsRead; + } + + // _isBlocked == whether we read fewer bytes than we asked for. + _isBlocked = (_bytesRead < _byteBufferSize); + + _charsRead += _decoder.GetChars( + _byteBuffer, + 0, + _bytesRead, + _charBuffer, + _charsRead); + } + while (_charsRead == 0); + + return _charsRead; + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs new file mode 100644 index 0000000000..050088ccb7 --- /dev/null +++ b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs @@ -0,0 +1,340 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// + /// Writes to the using the supplied . + /// It does not write the BOM and also does not close the stream. + /// + public class HttpResponseStreamWriter : TextWriter + { + private const int MinBufferSize = 128; + internal const int DefaultBufferSize = 16 * 1024; + + private Stream _stream; + private readonly Encoder _encoder; + private readonly ArrayPool _bytePool; + private readonly ArrayPool _charPool; + private readonly int _charBufferSize; + + private byte[] _byteBuffer; + private char[] _charBuffer; + + private int _charBufferCount; + private bool _disposed; + + public HttpResponseStreamWriter(Stream stream, Encoding encoding) + : this(stream, encoding, DefaultBufferSize, ArrayPool.Shared, ArrayPool.Shared) + { + } + + public HttpResponseStreamWriter(Stream stream, Encoding encoding, int bufferSize) + : this(stream, encoding, bufferSize, ArrayPool.Shared, ArrayPool.Shared) + { + } + + public HttpResponseStreamWriter( + Stream stream, + Encoding encoding, + int bufferSize, + ArrayPool bytePool, + ArrayPool charPool) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + Encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); + _bytePool = bytePool ?? throw new ArgumentNullException(nameof(bytePool)); + _charPool = charPool ?? throw new ArgumentNullException(nameof(charPool)); + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + if (!_stream.CanWrite) + { + throw new ArgumentException(Resources.HttpResponseStreamWriter_StreamNotWritable, nameof(stream)); + } + + _charBufferSize = bufferSize; + + _encoder = encoding.GetEncoder(); + _charBuffer = charPool.Rent(bufferSize); + + try + { + var requiredLength = encoding.GetMaxByteCount(bufferSize); + _byteBuffer = bytePool.Rent(requiredLength); + } + catch + { + charPool.Return(_charBuffer); + + if (_byteBuffer != null) + { + bytePool.Return(_byteBuffer); + } + + throw; + } + } + + public override Encoding Encoding { get; } + + public override void Write(char value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (_charBufferCount == _charBufferSize) + { + FlushInternal(flushEncoder: false); + } + + _charBuffer[_charBufferCount] = value; + _charBufferCount++; + } + + public override void Write(char[] values, int index, int count) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (values == null) + { + return; + } + + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + FlushInternal(flushEncoder: false); + } + + CopyToCharBuffer(values, ref index, ref count); + } + } + + public override void Write(string value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (value == null) + { + return; + } + + var count = value.Length; + var index = 0; + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + FlushInternal(flushEncoder: false); + } + + CopyToCharBuffer(value, ref index, ref count); + } + } + + public override async Task WriteAsync(char value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(flushEncoder: false); + } + + _charBuffer[_charBufferCount] = value; + _charBufferCount++; + } + + public override async Task WriteAsync(char[] values, int index, int count) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (values == null) + { + return; + } + + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(flushEncoder: false); + } + + CopyToCharBuffer(values, ref index, ref count); + } + } + + public override async Task WriteAsync(string value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + if (value == null) + { + return; + } + + var count = value.Length; + var index = 0; + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(flushEncoder: false); + } + + CopyToCharBuffer(value, ref index, ref count); + } + } + + // We want to flush the stream when Flush/FlushAsync is explicitly + // called by the user (example: from a Razor view). + + public override void Flush() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + FlushInternal(flushEncoder: true); + } + + public override Task FlushAsync() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + return FlushInternalAsync(flushEncoder: true); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + try + { + FlushInternal(flushEncoder: true); + } + finally + { + _bytePool.Return(_byteBuffer); + _charPool.Return(_charBuffer); + } + } + + base.Dispose(disposing); + } + + // Note: our FlushInternal method does NOT flush the underlying stream. This would result in + // chunking. + private void FlushInternal(bool flushEncoder) + { + if (_charBufferCount == 0) + { + return; + } + + var count = _encoder.GetBytes( + _charBuffer, + 0, + _charBufferCount, + _byteBuffer, + 0, + flush: flushEncoder); + + _charBufferCount = 0; + + if (count > 0) + { + _stream.Write(_byteBuffer, 0, count); + } + } + + // Note: our FlushInternalAsync method does NOT flush the underlying stream. This would result in + // chunking. + private async Task FlushInternalAsync(bool flushEncoder) + { + if (_charBufferCount == 0) + { + return; + } + + var count = _encoder.GetBytes( + _charBuffer, + 0, + _charBufferCount, + _byteBuffer, + 0, + flush: flushEncoder); + + _charBufferCount = 0; + + if (count > 0) + { + await _stream.WriteAsync(_byteBuffer, 0, count); + } + } + + private void CopyToCharBuffer(string value, ref int index, ref int count) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, count); + + value.CopyTo( + sourceIndex: index, + destination: _charBuffer, + destinationIndex: _charBufferCount, + count: remaining); + + _charBufferCount += remaining; + index += remaining; + count -= remaining; + } + + private void CopyToCharBuffer(char[] values, ref int index, ref int count) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, count); + + Buffer.BlockCopy( + src: values, + srcOffset: index * sizeof(char), + dst: _charBuffer, + dstOffset: _charBufferCount * sizeof(char), + count: remaining * sizeof(char)); + + _charBufferCount += remaining; + index += remaining; + count -= remaining; + } + } +} diff --git a/src/Http/WebUtilities/src/KeyValueAccumulator.cs b/src/Http/WebUtilities/src/KeyValueAccumulator.cs new file mode 100644 index 0000000000..5ae402e523 --- /dev/null +++ b/src/Http/WebUtilities/src/KeyValueAccumulator.cs @@ -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; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public struct KeyValueAccumulator + { + private Dictionary _accumulator; + private Dictionary> _expandingAccumulator; + + public void Append(string key, string value) + { + if (_accumulator == null) + { + _accumulator = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + StringValues values; + if (_accumulator.TryGetValue(key, out values)) + { + if (values.Count == 0) + { + // Marker entry for this key to indicate entry already in expanding list dictionary + _expandingAccumulator[key].Add(value); + } + else if (values.Count == 1) + { + // Second value for this key + _accumulator[key] = new string[] { values[0], value }; + } + else + { + // Third value for this key + // Add zero count entry and move to data to expanding list dictionary + _accumulator[key] = default(StringValues); + + if (_expandingAccumulator == null) + { + _expandingAccumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + // Already 3 entries so use starting allocated as 8; then use List's expansion mechanism for more + var list = new List(8); + var array = values.ToArray(); + + list.Add(array[0]); + list.Add(array[1]); + list.Add(value); + + _expandingAccumulator[key] = list; + } + } + else + { + // First value for this key + _accumulator[key] = new StringValues(value); + } + + ValueCount++; + } + + public bool HasValues => ValueCount > 0; + + public int KeyCount => _accumulator?.Count ?? 0; + + public int ValueCount { get; private set; } + + public Dictionary GetResults() + { + if (_expandingAccumulator != null) + { + // Coalesce count 3+ multi-value entries into _accumulator dictionary + foreach (var entry in _expandingAccumulator) + { + _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); + } + } + + return _accumulator ?? new Dictionary(0, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj b/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj new file mode 100644 index 0000000000..3c7d2d8255 --- /dev/null +++ b/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj @@ -0,0 +1,18 @@ + + + + ASP.NET Core utilities, such as for working with forms, multipart messages, and query strings. + netstandard2.0 + $(DefineConstants);WebEncoders_In_WebUtilities + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + diff --git a/src/Http/WebUtilities/src/MultipartBoundary.cs b/src/Http/WebUtilities/src/MultipartBoundary.cs new file mode 100644 index 0000000000..0da1303835 --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartBoundary.cs @@ -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 System; +using System.Text; + +namespace Microsoft.AspNetCore.WebUtilities +{ + internal class MultipartBoundary + { + private readonly int[] _skipTable = new int[256]; + private readonly string _boundary; + private bool _expectLeadingCrlf; + + public MultipartBoundary(string boundary, bool expectLeadingCrlf = true) + { + if (boundary == null) + { + throw new ArgumentNullException(nameof(boundary)); + } + + _boundary = boundary; + _expectLeadingCrlf = expectLeadingCrlf; + Initialize(_boundary, _expectLeadingCrlf); + } + + private void Initialize(string boundary, bool expectLeadingCrlf) + { + if (expectLeadingCrlf) + { + BoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary); + } + else + { + BoundaryBytes = Encoding.UTF8.GetBytes("--" + boundary); + } + FinalBoundaryLength = BoundaryBytes.Length + 2; // Include the final '--' terminator. + + var length = BoundaryBytes.Length; + for (var i = 0; i < _skipTable.Length; ++i) + { + _skipTable[i] = length; + } + for (var i = 0; i < length; ++i) + { + _skipTable[BoundaryBytes[i]] = Math.Max(1, length - 1 - i); + } + } + + public int GetSkipValue(byte input) + { + return _skipTable[input]; + } + + public bool ExpectLeadingCrlf + { + get { return _expectLeadingCrlf; } + set + { + if (value != _expectLeadingCrlf) + { + _expectLeadingCrlf = value; + Initialize(_boundary, _expectLeadingCrlf); + } + } + } + + public byte[] BoundaryBytes { get; private set; } + + public int FinalBoundaryLength { get; private set; } + } +} diff --git a/src/Http/WebUtilities/src/MultipartReader.cs b/src/Http/WebUtilities/src/MultipartReader.cs new file mode 100644 index 0000000000..2da50a5360 --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartReader.cs @@ -0,0 +1,118 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + // https://www.ietf.org/rfc/rfc2046.txt + public class MultipartReader + { + public const int DefaultHeadersCountLimit = 16; + public const int DefaultHeadersLengthLimit = 1024 * 16; + private const int DefaultBufferSize = 1024 * 4; + + private readonly BufferedReadStream _stream; + private readonly MultipartBoundary _boundary; + private MultipartReaderStream _currentStream; + + public MultipartReader(string boundary, Stream stream) + : this(boundary, stream, DefaultBufferSize) + { + } + + public MultipartReader(string boundary, Stream stream, int bufferSize) + { + if (boundary == null) + { + throw new ArgumentNullException(nameof(boundary)); + } + + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (bufferSize < boundary.Length + 8) // Size of the boundary + leading and trailing CRLF + leading and trailing '--' markers. + { + throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "Insufficient buffer space, the buffer must be larger than the boundary: " + boundary); + } + _stream = new BufferedReadStream(stream, bufferSize); + _boundary = new MultipartBoundary(boundary, false); + // This stream will drain any preamble data and remove the first boundary marker. + // TODO: HeadersLengthLimit can't be modified until after the constructor. + _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit }; + } + + /// + /// The limit for the number of headers to read. + /// + public int HeadersCountLimit { get; set; } = DefaultHeadersCountLimit; + + /// + /// The combined size limit for headers per multipart section. + /// + public int HeadersLengthLimit { get; set; } = DefaultHeadersLengthLimit; + + /// + /// The optional limit for the total response body length. + /// + public long? BodyLengthLimit { get; set; } + + public async Task ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken()) + { + // Drain the prior section. + await _currentStream.DrainAsync(cancellationToken); + // If we're at the end return null + if (_currentStream.FinalBoundaryFound) + { + // There may be trailer data after the last boundary. + await _stream.DrainAsync(HeadersLengthLimit, cancellationToken); + return null; + } + var headers = await ReadHeadersAsync(cancellationToken); + _boundary.ExpectLeadingCrlf = true; + _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = BodyLengthLimit }; + long? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null; + return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset }; + } + + private async Task> ReadHeadersAsync(CancellationToken cancellationToken) + { + int totalSize = 0; + var accumulator = new KeyValueAccumulator(); + var line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken); + while (!string.IsNullOrEmpty(line)) + { + if (HeadersLengthLimit - totalSize < line.Length) + { + throw new InvalidDataException($"Multipart headers length limit {HeadersLengthLimit} exceeded."); + } + totalSize += line.Length; + int splitIndex = line.IndexOf(':'); + if (splitIndex <= 0) + { + throw new InvalidDataException($"Invalid header line: {line}"); + } + + var name = line.Substring(0, splitIndex); + var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); + accumulator.Append(name, value); + if (accumulator.KeyCount > HeadersCountLimit) + { + throw new InvalidDataException($"Multipart headers count limit {HeadersCountLimit} exceeded."); + } + + line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken); + } + + return accumulator.GetResults(); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/MultipartReaderStream.cs b/src/Http/WebUtilities/src/MultipartReaderStream.cs new file mode 100644 index 0000000000..7952bd34b2 --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartReaderStream.cs @@ -0,0 +1,336 @@ +// 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.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + internal class MultipartReaderStream : Stream + { + private readonly MultipartBoundary _boundary; + private readonly BufferedReadStream _innerStream; + private readonly ArrayPool _bytePool; + + private readonly long _innerOffset; + private long _position; + private long _observedLength; + private bool _finished; + + /// + /// Creates a stream that reads until it reaches the given boundary pattern. + /// + /// The . + /// The boundary pattern to use. + public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary) + : this(stream, boundary, ArrayPool.Shared) + { + } + + /// + /// Creates a stream that reads until it reaches the given boundary pattern. + /// + /// The . + /// The boundary pattern to use. + /// The ArrayPool pool to use for temporary byte arrays. + public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary, ArrayPool bytePool) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (boundary == null) + { + throw new ArgumentNullException(nameof(boundary)); + } + + _bytePool = bytePool; + _innerStream = stream; + _innerOffset = _innerStream.CanSeek ? _innerStream.Position : 0; + _boundary = boundary; + } + + public bool FinalBoundaryFound { get; private set; } + + public long? LengthLimit { get; set; } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return _innerStream.CanSeek; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _observedLength; } + } + + public override long Position + { + get { return _position; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be positive."); + } + if (value > _observedLength) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be less than length."); + } + _position = value; + if (_position < _observedLength) + { + _finished = false; + } + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + else // if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + return Position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + private void PositionInnerStream() + { + if (_innerStream.CanSeek && _innerStream.Position != (_innerOffset + _position)) + { + _innerStream.Position = _innerOffset + _position; + } + } + + private int UpdatePosition(int read) + { + _position += read; + if (_observedLength < _position) + { + _observedLength = _position; + if (LengthLimit.HasValue && _observedLength > LengthLimit.Value) + { + throw new InvalidDataException($"Multipart body length limit {LengthLimit.Value} exceeded."); + } + } + return read; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_finished) + { + return 0; + } + + PositionInnerStream(); + if (!_innerStream.EnsureBuffered(_boundary.FinalBoundaryLength)) + { + throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + } + var bufferedData = _innerStream.BufferedData; + + // scan for a boundary match, full or partial. + int read; + if (SubMatch(bufferedData, _boundary.BoundaryBytes, out var matchOffset, out var matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) + { + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); + } + + var length = _boundary.BoundaryBytes.Length; + Debug.Assert(matchCount == length); + + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + var boundary = _bytePool.Rent(length); + read = _innerStream.Read(boundary, 0, length); + _bytePool.Return(boundary); + Debug.Assert(read == length); // It should have all been buffered + + var remainder = _innerStream.ReadLine(lengthLimit: 100); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; + } + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); + _finished = true; + return 0; + } + + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_finished) + { + return 0; + } + + PositionInnerStream(); + if (!await _innerStream.EnsureBufferedAsync(_boundary.FinalBoundaryLength, cancellationToken)) + { + throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + } + var bufferedData = _innerStream.BufferedData; + + // scan for a boundary match, full or partial. + int matchOffset; + int matchCount; + int read; + if (SubMatch(bufferedData, _boundary.BoundaryBytes, out matchOffset, out matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) + { + // Sync, it's already buffered + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); + } + + var length = _boundary.BoundaryBytes.Length; + Debug.Assert(matchCount == length); + + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + var boundary = _bytePool.Rent(length); + read = _innerStream.Read(boundary, 0, length); + _bytePool.Return(boundary); + Debug.Assert(read == length); // It should have all been buffered + + var remainder = await _innerStream.ReadLineAsync(lengthLimit: 100, cancellationToken: cancellationToken); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; + } + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); + + _finished = true; + return 0; + } + + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } + + // Does segment1 contain all of matchBytes, or does it end with the start of matchBytes? + // 1: AAAAABBBBBCCCCC + // 2: BBBBB + // Or: + // 1: AAAAABBB + // 2: BBBBB + private bool SubMatch(ArraySegment segment1, byte[] matchBytes, out int matchOffset, out int matchCount) + { + // clear matchCount to zero + matchCount = 0; + + // case 1: does segment1 fully contain matchBytes? + { + var matchBytesLengthMinusOne = matchBytes.Length - 1; + var matchBytesLastByte = matchBytes[matchBytesLengthMinusOne]; + var segmentEndMinusMatchBytesLength = segment1.Offset + segment1.Count - matchBytes.Length; + + matchOffset = segment1.Offset; + while (matchOffset < segmentEndMinusMatchBytesLength) + { + var lookaheadTailChar = segment1.Array[matchOffset + matchBytesLengthMinusOne]; + if (lookaheadTailChar == matchBytesLastByte && + CompareBuffers(segment1.Array, matchOffset, matchBytes, 0, matchBytesLengthMinusOne) == 0) + { + matchCount = matchBytes.Length; + return true; + } + matchOffset += _boundary.GetSkipValue(lookaheadTailChar); + } + } + + // case 2: does segment1 end with the start of matchBytes? + var segmentEnd = segment1.Offset + segment1.Count; + + matchCount = 0; + for (; matchOffset < segmentEnd; matchOffset++) + { + var countLimit = segmentEnd - matchOffset; + for (matchCount = 0; matchCount < matchBytes.Length && matchCount < countLimit; matchCount++) + { + if (matchBytes[matchCount] != segment1.Array[matchOffset + matchCount]) + { + matchCount = 0; + break; + } + } + if (matchCount > 0) + { + break; + } + } + return matchCount > 0; + } + + private static int CompareBuffers(byte[] buffer1, int offset1, byte[] buffer2, int offset2, int count) + { + for (; count-- > 0; offset1++, offset2++) + { + if (buffer1[offset1] != buffer2[offset2]) + { + return buffer1[offset1] - buffer2[offset2]; + } + } + return 0; + } + } +} diff --git a/src/Http/WebUtilities/src/MultipartSection.cs b/src/Http/WebUtilities/src/MultipartSection.cs new file mode 100644 index 0000000000..96138c630a --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartSection.cs @@ -0,0 +1,48 @@ +// 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.IO; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class MultipartSection + { + public string ContentType + { + get + { + StringValues values; + if (Headers.TryGetValue("Content-Type", out values)) + { + return values; + } + return null; + } + } + + public string ContentDisposition + { + get + { + StringValues values; + if (Headers.TryGetValue("Content-Disposition", out values)) + { + return values; + } + return null; + } + } + + public Dictionary Headers { get; set; } + + public Stream Body { get; set; } + + /// + /// The position where the body starts in the total multipart body. + /// This may not be available if the total multipart body is not seekable. + /// + public long? BaseStreamOffset { get; set; } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs b/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs new file mode 100644 index 0000000000..826ced168e --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs @@ -0,0 +1,74 @@ +// 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.Net.Http.Headers; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// + /// Various extensions for converting multipart sections + /// + public static class MultipartSectionConverterExtensions + { + /// + /// Converts the section to a file section + /// + /// The section to convert + /// A file section + public static FileMultipartSection AsFileSection(this MultipartSection section) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); + } + + try + { + return new FileMultipartSection(section); + } + catch + { + return null; + } + } + + /// + /// Converts the section to a form section + /// + /// The section to convert + /// A form section + public static FormMultipartSection AsFormDataSection(this MultipartSection section) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); + } + + try + { + return new FormMultipartSection(section); + } + catch + { + return null; + } + } + + /// + /// Retrieves and parses the content disposition header from a section + /// + /// The section from which to retrieve + /// A if the header was found, null otherwise + public static ContentDispositionHeaderValue GetContentDispositionHeader(this MultipartSection section) + { + ContentDispositionHeaderValue header; + if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out header)) + { + return null; + } + + return header; + } + } +} diff --git a/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs b/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs new file mode 100644 index 0000000000..463a8d88d6 --- /dev/null +++ b/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs @@ -0,0 +1,49 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.WebUtilities +{ + /// + /// Various extension methods for dealing with the section body stream + /// + public static class MultipartSectionStreamExtensions + { + /// + /// Reads the body of the section as a string + /// + /// The section to read from + /// The body steam as string + public static async Task ReadAsStringAsync(this MultipartSection section) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); + } + + MediaTypeHeaderValue sectionMediaType; + MediaTypeHeaderValue.TryParse(section.ContentType, out sectionMediaType); + + Encoding streamEncoding = sectionMediaType?.Encoding; + if (streamEncoding == null || streamEncoding == Encoding.UTF7) + { + streamEncoding = Encoding.UTF8; + } + + using (var reader = new StreamReader( + section.Body, + streamEncoding, + detectEncodingFromByteOrderMarks: true, + bufferSize: 1024, + leaveOpen: true)) + { + return await reader.ReadToEndAsync(); + } + } + } +} diff --git a/src/Http/WebUtilities/src/Properties/AssemblyInfo.cs b/src/Http/WebUtilities/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..aa80ef1d7e --- /dev/null +++ b/src/Http/WebUtilities/src/Properties/AssemblyInfo.cs @@ -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.AspNetCore.WebUtilities.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Http/WebUtilities/src/QueryHelpers.cs b/src/Http/WebUtilities/src/QueryHelpers.cs new file mode 100644 index 0000000000..6bd1a0bb82 --- /dev/null +++ b/src/Http/WebUtilities/src/QueryHelpers.cs @@ -0,0 +1,191 @@ +// 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.Text; +using System.Text.Encodings.Web; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public static class QueryHelpers + { + /// + /// Append the given query key and value to the URI. + /// + /// The base URI. + /// The name of the query key. + /// The query value. + /// The combined result. + public static string AddQueryString(string uri, string name, string value) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + return AddQueryString( + uri, new[] { new KeyValuePair(name, value) }); + } + + /// + /// Append the given query keys and values to the uri. + /// + /// The base uri. + /// A collection of name value query pairs to append. + /// The combined result. + public static string AddQueryString(string uri, IDictionary queryString) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (queryString == null) + { + throw new ArgumentNullException(nameof(queryString)); + } + + return AddQueryString(uri, (IEnumerable>)queryString); + } + + private static string AddQueryString( + string uri, + IEnumerable> queryString) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (queryString == null) + { + throw new ArgumentNullException(nameof(queryString)); + } + + var anchorIndex = uri.IndexOf('#'); + var uriToBeAppended = uri; + var anchorText = ""; + // If there is an anchor, then the query string must be inserted before its first occurance. + if (anchorIndex != -1) + { + anchorText = uri.Substring(anchorIndex); + uriToBeAppended = uri.Substring(0, anchorIndex); + } + + var queryIndex = uriToBeAppended.IndexOf('?'); + var hasQuery = queryIndex != -1; + + var sb = new StringBuilder(); + sb.Append(uriToBeAppended); + foreach (var parameter in queryString) + { + sb.Append(hasQuery ? '&' : '?'); + sb.Append(UrlEncoder.Default.Encode(parameter.Key)); + sb.Append('='); + sb.Append(UrlEncoder.Default.Encode(parameter.Value)); + hasQuery = true; + } + + sb.Append(anchorText); + return sb.ToString(); + } + + /// + /// Parse a query string into its component key and value parts. + /// + /// The raw query string value, with or without the leading '?'. + /// A collection of parsed keys and values. + public static Dictionary ParseQuery(string queryString) + { + var result = ParseNullableQuery(queryString); + + if (result == null) + { + return new Dictionary(); + } + + return result; + } + + + /// + /// Parse a query string into its component key and value parts. + /// + /// The raw query string value, with or without the leading '?'. + /// A collection of parsed keys and values, null if there are no entries. + public static Dictionary ParseNullableQuery(string queryString) + { + var accumulator = new KeyValueAccumulator(); + + if (string.IsNullOrEmpty(queryString) || queryString == "?") + { + return null; + } + + int scanIndex = 0; + if (queryString[0] == '?') + { + scanIndex = 1; + } + + int textLength = queryString.Length; + int equalIndex = queryString.IndexOf('='); + if (equalIndex == -1) + { + equalIndex = textLength; + } + while (scanIndex < textLength) + { + int delimiterIndex = queryString.IndexOf('&', scanIndex); + if (delimiterIndex == -1) + { + delimiterIndex = textLength; + } + if (equalIndex < delimiterIndex) + { + while (scanIndex != equalIndex && char.IsWhiteSpace(queryString[scanIndex])) + { + ++scanIndex; + } + string name = queryString.Substring(scanIndex, equalIndex - scanIndex); + string value = queryString.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1); + accumulator.Append( + Uri.UnescapeDataString(name.Replace('+', ' ')), + Uri.UnescapeDataString(value.Replace('+', ' '))); + equalIndex = queryString.IndexOf('=', delimiterIndex); + if (equalIndex == -1) + { + equalIndex = textLength; + } + } + else + { + if (delimiterIndex > scanIndex) + { + accumulator.Append(queryString.Substring(scanIndex, delimiterIndex - scanIndex), string.Empty); + } + } + scanIndex = delimiterIndex + 1; + } + + if (!accumulator.HasValues) + { + return null; + } + + return accumulator.GetResults(); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/ReasonPhrases.cs b/src/Http/WebUtilities/src/ReasonPhrases.cs new file mode 100644 index 0000000000..3aab17079d --- /dev/null +++ b/src/Http/WebUtilities/src/ReasonPhrases.cs @@ -0,0 +1,87 @@ +// 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; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public static class ReasonPhrases + { + // Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + private static IDictionary Phrases = new Dictionary() + { + { 100, "Continue" }, + { 101, "Switching Protocols" }, + { 102, "Processing" }, + + { 200, "OK" }, + { 201, "Created" }, + { 202, "Accepted" }, + { 203, "Non-Authoritative Information" }, + { 204, "No Content" }, + { 205, "Reset Content" }, + { 206, "Partial Content" }, + { 207, "Multi-Status" }, + { 208, "Already Reported" }, + { 226, "IM Used" }, + + { 300, "Multiple Choices" }, + { 301, "Moved Permanently" }, + { 302, "Found" }, + { 303, "See Other" }, + { 304, "Not Modified" }, + { 305, "Use Proxy" }, + { 306, "Switch Proxy" }, + { 307, "Temporary Redirect" }, + { 308, "Permanent Redirect" }, + + { 400, "Bad Request" }, + { 401, "Unauthorized" }, + { 402, "Payment Required" }, + { 403, "Forbidden" }, + { 404, "Not Found" }, + { 405, "Method Not Allowed" }, + { 406, "Not Acceptable" }, + { 407, "Proxy Authentication Required" }, + { 408, "Request Timeout" }, + { 409, "Conflict" }, + { 410, "Gone" }, + { 411, "Length Required" }, + { 412, "Precondition Failed" }, + { 413, "Payload Too Large" }, + { 414, "URI Too Long" }, + { 415, "Unsupported Media Type" }, + { 416, "Range Not Satisfiable" }, + { 417, "Expectation Failed" }, + { 418, "I'm a teapot" }, + { 419, "Authentication Timeout" }, + { 421, "Misdirected Request" }, + { 422, "Unprocessable Entity" }, + { 423, "Locked" }, + { 424, "Failed Dependency" }, + { 426, "Upgrade Required" }, + { 428, "Precondition Required" }, + { 429, "Too Many Requests" }, + { 431, "Request Header Fields Too Large" }, + { 451, "Unavailable For Legal Reasons" }, + + { 500, "Internal Server Error" }, + { 501, "Not Implemented" }, + { 502, "Bad Gateway" }, + { 503, "Service Unavailable" }, + { 504, "Gateway Timeout" }, + { 505, "HTTP Version Not Supported" }, + { 506, "Variant Also Negotiates" }, + { 507, "Insufficient Storage" }, + { 508, "Loop Detected" }, + { 510, "Not Extended" }, + { 511, "Network Authentication Required" }, + }; + + public static string GetReasonPhrase(int statusCode) + { + string phrase; + return Phrases.TryGetValue(statusCode, out phrase) ? phrase : string.Empty; + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/Resources.Designer.cs b/src/Http/WebUtilities/src/Resources.Designer.cs new file mode 100644 index 0000000000..7972e005d0 --- /dev/null +++ b/src/Http/WebUtilities/src/Resources.Designer.cs @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNetCore.WebUtilities { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.WebUtilities.Resources", typeof(Resources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The stream must support reading.. + /// + internal static string HttpRequestStreamReader_StreamNotReadable { + get { + return ResourceManager.GetString("HttpRequestStreamReader_StreamNotReadable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The stream must support writing.. + /// + internal static string HttpResponseStreamWriter_StreamNotWritable { + get { + return ResourceManager.GetString("HttpResponseStreamWriter_StreamNotWritable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid {0}, {1} or {2} length.. + /// + internal static string WebEncoders_InvalidCountOffsetOrLength { + get { + return ResourceManager.GetString("WebEncoders_InvalidCountOffsetOrLength", resourceCulture); + } + } + } +} diff --git a/src/Http/WebUtilities/src/Resources.resx b/src/Http/WebUtilities/src/Resources.resx new file mode 100644 index 0000000000..a32d2db5cc --- /dev/null +++ b/src/Http/WebUtilities/src/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The stream must support reading. + + + The stream must support writing. + + + Invalid {0}, {1} or {2} length. + + \ No newline at end of file diff --git a/src/Http/WebUtilities/src/StreamHelperExtensions.cs b/src/Http/WebUtilities/src/StreamHelperExtensions.cs new file mode 100644 index 0000000000..e2c16a9cf2 --- /dev/null +++ b/src/Http/WebUtilities/src/StreamHelperExtensions.cs @@ -0,0 +1,51 @@ +// 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.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public static class StreamHelperExtensions + { + private const int _maxReadBufferSize = 1024 * 4; + + public static Task DrainAsync(this Stream stream, CancellationToken cancellationToken) + { + return stream.DrainAsync(ArrayPool.Shared, null, cancellationToken); + } + + public static Task DrainAsync(this Stream stream, long? limit, CancellationToken cancellationToken) + { + return stream.DrainAsync(ArrayPool.Shared, limit, cancellationToken); + } + + public static async Task DrainAsync(this Stream stream, ArrayPool bytePool, long? limit, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var buffer = bytePool.Rent(_maxReadBufferSize); + long total = 0; + try + { + var read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); + while (read > 0) + { + // Not all streams support cancellation directly. + cancellationToken.ThrowIfCancellationRequested(); + if (limit.HasValue && limit.Value - total < read) + { + throw new InvalidDataException($"The stream exceeded the data limit {limit.Value}."); + } + total += read; + read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); + } + } + finally + { + bytePool.Return(buffer); + } + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/src/baseline.netcore.json b/src/Http/WebUtilities/src/baseline.netcore.json new file mode 100644 index 0000000000..896fe0fcb3 --- /dev/null +++ b/src/Http/WebUtilities/src/baseline.netcore.json @@ -0,0 +1,2272 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.WebUtilities, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.WebUtilities.WebEncoders", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Base64UrlDecode", + "Parameters": [ + { + "Name": "input", + "Type": "System.String" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlDecode", + "Parameters": [ + { + "Name": "input", + "Type": "System.String" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlDecode", + "Parameters": [ + { + "Name": "input", + "Type": "System.String" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "buffer", + "Type": "System.Char[]" + }, + { + "Name": "bufferOffset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetArraySizeRequiredToDecode", + "Parameters": [ + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlEncode", + "Parameters": [ + { + "Name": "input", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlEncode", + "Parameters": [ + { + "Name": "input", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Base64UrlEncode", + "Parameters": [ + { + "Name": "input", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "output", + "Type": "System.Char[]" + }, + { + "Name": "outputOffset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetArraySizeRequiredToEncode", + "Parameters": [ + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.Base64UrlTextEncoder", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Encode", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Decode", + "Parameters": [ + { + "Name": "text", + "Type": "System.String" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.BufferedReadStream", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.IO.Stream", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_BufferedData", + "Parameters": [], + "ReturnType": "System.ArraySegment", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanRead", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanSeek", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanTimeout", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanWrite", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Length", + "Parameters": [], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Position", + "Parameters": [], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Position", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Seek", + "Parameters": [ + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "origin", + "Type": "System.IO.SeekOrigin" + } + ], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "disposing", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Flush", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FlushAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnsureBuffered", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnsureBufferedAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnsureBuffered", + "Parameters": [ + { + "Name": "minCount", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EnsureBufferedAsync", + "Parameters": [ + { + "Name": "minCount", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadLine", + "Parameters": [ + { + "Name": "lengthLimit", + "Type": "System.Int32" + } + ], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadLineAsync", + "Parameters": [ + { + "Name": "lengthLimit", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.IO.Stream", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_InMemory", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TempFileName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanRead", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanSeek", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CanWrite", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Length", + "Parameters": [], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Position", + "Parameters": [], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Position", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Seek", + "Parameters": [ + { + "Name": "offset", + "Type": "System.Int64" + }, + { + "Name": "origin", + "Type": "System.IO.SeekOrigin" + } + ], + "ReturnType": "System.Int64", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Byte[]" + }, + { + "Name": "offset", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetLength", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Flush", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "disposing", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "memoryThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Nullable" + }, + { + "Name": "tempFileDirectoryAccessor", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "memoryThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Nullable" + }, + { + "Name": "tempFileDirectoryAccessor", + "Type": "System.Func" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "memoryThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Nullable" + }, + { + "Name": "tempFileDirectory", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "inner", + "Type": "System.IO.Stream" + }, + { + "Name": "memoryThreshold", + "Type": "System.Int32" + }, + { + "Name": "bufferLimit", + "Type": "System.Nullable" + }, + { + "Name": "tempFileDirectory", + "Type": "System.String" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.FileMultipartSection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Section", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.WebUtilities.MultipartSection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileStream", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FileName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + }, + { + "Name": "header", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.FormMultipartSection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Section", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.WebUtilities.MultipartSection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetValueAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + }, + { + "Name": "header", + "Type": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.FormReader", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.IDisposable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ValueCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ValueCountLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_KeyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_KeyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ValueLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadNextPair", + "Parameters": [], + "ReturnType": "System.Nullable>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadNextPairAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task>>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadForm", + "Parameters": [], + "ReturnType": "System.Collections.Generic.Dictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadFormAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "data", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "data", + "Type": "System.String" + }, + { + "Name": "charPool", + "Type": "System.Buffers.ArrayPool" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "charPool", + "Type": "System.Buffers.ArrayPool" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultValueCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "1024" + }, + { + "Kind": "Field", + "Name": "DefaultKeyLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "2048" + }, + { + "Kind": "Field", + "Name": "DefaultValueLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "4194304" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.HttpRequestStreamReader", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.IO.TextReader", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "disposing", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Peek", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Char[]" + }, + { + "Name": "index", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadAsync", + "Parameters": [ + { + "Name": "buffer", + "Type": "System.Char[]" + }, + { + "Name": "index", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool" + }, + { + "Name": "charPool", + "Type": "System.Buffers.ArrayPool" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.HttpResponseStreamWriter", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.IO.TextWriter", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Encoding", + "Parameters": [], + "ReturnType": "System.Text.Encoding", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "value", + "Type": "System.Char" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "values", + "Type": "System.Char[]" + }, + { + "Name": "index", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "value", + "Type": "System.Char" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "values", + "Type": "System.Char[]" + }, + { + "Name": "index", + "Type": "System.Int32" + }, + { + "Name": "count", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAsync", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Flush", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FlushAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [ + { + "Name": "disposing", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "encoding", + "Type": "System.Text.Encoding" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool" + }, + { + "Name": "charPool", + "Type": "System.Buffers.ArrayPool" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.KeyValueAccumulator", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasValues", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_KeyCount", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueCount", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetResults", + "Parameters": [], + "ReturnType": "System.Collections.Generic.Dictionary", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.MultipartReader", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_HeadersCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HeadersCountLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HeadersLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HeadersLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BodyLengthLimit", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BodyLengthLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadNextSectionAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "boundary", + "Type": "System.String" + }, + { + "Name": "stream", + "Type": "System.IO.Stream" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "boundary", + "Type": "System.String" + }, + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "bufferSize", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultHeadersCountLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "16" + }, + { + "Kind": "Field", + "Name": "DefaultHeadersLengthLimit", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "16384" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.MultipartSection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ContentType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContentDisposition", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "System.Collections.Generic.Dictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Headers", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.Dictionary" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Body", + "Parameters": [], + "ReturnType": "System.IO.Stream", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Body", + "Parameters": [ + { + "Name": "value", + "Type": "System.IO.Stream" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BaseStreamOffset", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BaseStreamOffset", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.MultipartSectionConverterExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AsFileSection", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "ReturnType": "Microsoft.AspNetCore.WebUtilities.FileMultipartSection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AsFormDataSection", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "ReturnType": "Microsoft.AspNetCore.WebUtilities.FormMultipartSection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetContentDispositionHeader", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "ReturnType": "Microsoft.Net.Http.Headers.ContentDispositionHeaderValue", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.MultipartSectionStreamExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ReadAsStringAsync", + "Parameters": [ + { + "Name": "section", + "Type": "Microsoft.AspNetCore.WebUtilities.MultipartSection" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.QueryHelpers", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddQueryString", + "Parameters": [ + { + "Name": "uri", + "Type": "System.String" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddQueryString", + "Parameters": [ + { + "Name": "uri", + "Type": "System.String" + }, + { + "Name": "queryString", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseQuery", + "Parameters": [ + { + "Name": "queryString", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.Dictionary", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseNullableQuery", + "Parameters": [ + { + "Name": "queryString", + "Type": "System.String" + } + ], + "ReturnType": "System.Collections.Generic.Dictionary", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.ReasonPhrases", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetReasonPhrase", + "Parameters": [ + { + "Name": "statusCode", + "Type": "System.Int32" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebUtilities.StreamHelperExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DrainAsync", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DrainAsync", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "limit", + "Type": "System.Nullable" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DrainAsync", + "Parameters": [ + { + "Name": "stream", + "Type": "System.IO.Stream" + }, + { + "Name": "bytePool", + "Type": "System.Buffers.ArrayPool" + }, + { + "Name": "limit", + "Type": "System.Nullable" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs b/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs new file mode 100644 index 0000000000..a83f1574eb --- /dev/null +++ b/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs @@ -0,0 +1,299 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class FileBufferingReadStreamTests + { + private Stream MakeStream(int size) + { + // TODO: Fill with random data? Make readonly? + return new MemoryStream(new byte[size]); + } + + [Fact] + public void FileBufferingReadStream_Properties_ExpectedValues() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024, null, Directory.GetCurrentDirectory())) + { + Assert.True(stream.CanRead); + Assert.True(stream.CanSeek); + Assert.False(stream.CanWrite); + Assert.Equal(0, stream.Length); // Nothing buffered yet + Assert.Equal(0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + } + } + + [Fact] + public void FileBufferingReadStream_SyncReadUnderThreshold_DoesntCreateFile() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024 * 3, null, Directory.GetCurrentDirectory())) + { + var bytes = new byte[1000]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read2 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read3 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(0, read3); + } + } + + [Fact] + public void FileBufferingReadStream_SyncReadOverThreshold_CreatesFile() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory())) + { + var bytes = new byte[1000]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName; + Assert.True(File.Exists(tempFileName)); + + var read2 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.True(File.Exists(tempFileName)); + + var read3 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(0, read3); + } + + Assert.False(File.Exists(tempFileName)); + } + + [Fact] + public void FileBufferingReadStream_SyncReadWithInMemoryLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024, 900, Directory.GetCurrentDirectory())) + { + var bytes = new byte[500]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var exception = Assert.Throws(() => stream.Read(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + Assert.False(File.Exists(stream.TempFileName)); + } + } + + [Fact] + public void FileBufferingReadStream_SyncReadWithOnDiskLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 512, 1024, GetCurrentDirectory())) + { + var bytes = new byte[500]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName; + Assert.True(File.Exists(tempFileName)); + + var exception = Assert.Throws(() => stream.Read(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.False(File.Exists(tempFileName)); + } + + Assert.False(File.Exists(tempFileName)); + } + + /////////////////// + + [Fact] + public async Task FileBufferingReadStream_AsyncReadUnderThreshold_DoesntCreateFile() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024 * 3, null, Directory.GetCurrentDirectory())) + { + var bytes = new byte[1000]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read2 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read3 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(0, read3); + } + } + + [Fact] + public async Task FileBufferingReadStream_AsyncReadOverThreshold_CreatesFile() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory())) + { + var bytes = new byte[1000]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName; + Assert.True(File.Exists(tempFileName)); + + var read2 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.True(File.Exists(tempFileName)); + + var read3 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(0, read3); + } + + Assert.False(File.Exists(tempFileName)); + } + + [Fact] + public async Task FileBufferingReadStream_AsyncReadWithInMemoryLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024, 900, Directory.GetCurrentDirectory())) + { + var bytes = new byte[500]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var exception = await Assert.ThrowsAsync(() => stream.ReadAsync(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + Assert.False(File.Exists(stream.TempFileName)); + } + } + + [Fact] + public async Task FileBufferingReadStream_AsyncReadWithOnDiskLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 512, 1024, GetCurrentDirectory())) + { + var bytes = new byte[500]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName; + Assert.True(File.Exists(tempFileName)); + + var exception = await Assert.ThrowsAsync(() => stream.ReadAsync(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.False(File.Exists(tempFileName)); + } + + Assert.False(File.Exists(tempFileName)); + } + + private static string GetCurrentDirectory() + { + return AppContext.BaseDirectory; + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/FormReaderAsyncTest.cs b/src/Http/WebUtilities/test/FormReaderAsyncTest.cs new file mode 100644 index 0000000000..0a7b5e20a9 --- /dev/null +++ b/src/Http/WebUtilities/test/FormReaderAsyncTest.cs @@ -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.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class FormReaderAsyncTest : FormReaderTests + { + protected override async Task> ReadFormAsync(FormReader reader) + { + return await reader.ReadFormAsync(); + } + + protected override async Task?> ReadPair(FormReader reader) + { + return await reader.ReadNextPairAsync(); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/FormReaderTests.cs b/src/Http/WebUtilities/test/FormReaderTests.cs new file mode 100644 index 0000000000..134efbb515 --- /dev/null +++ b/src/Http/WebUtilities/test/FormReaderTests.cs @@ -0,0 +1,230 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class FormReaderTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyKeyAtEndAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "=bar"); + + var formCollection = await ReadFormAsync(new FormReader(body)); + + Assert.Equal("bar", formCollection[""].ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "=bar&baz=2"); + + var formCollection = await ReadFormAsync(new FormReader(body)); + + Assert.Equal("bar", formCollection[""].ToString()); + Assert.Equal("2", formCollection["baz"].ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyValuedAtEndAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo="); + + var formCollection = await ReadFormAsync(new FormReader(body)); + + Assert.Equal("", formCollection["foo"].ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyValuedWithAdditionalEntryAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=&baz=2"); + + var formCollection = await ReadFormAsync(new FormReader(body)); + + Assert.Equal("", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["baz"].ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitMet_Success(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3"); + + var formCollection = await ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 }); + + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["bar"].ToString()); + Assert.Equal("3", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceeded_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&baz=2&bar=3&baz=4&baf=5"); + + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 })); + Assert.Equal("Form value count limit 3 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceededSameKey_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "baz=1&baz=2&baz=3&baz=4"); + + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 })); + Assert.Equal("Form value count limit 3 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_KeyLengthLimitMet_Success(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3&baz=4"); + + var formCollection = await ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 }); + + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["bar"].ToString()); + Assert.Equal("3,4", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_KeyLengthLimitExceeded_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&baz1234567890=2"); + + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 })); + Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueLengthLimitMet_Success(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&bar=1234567890&baz=3&baz=4"); + + var formCollection = await ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 }); + + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("1234567890", formCollection["bar"].ToString()); + Assert.Equal("3,4", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueLengthLimitExceeded_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&baz=1234567890123"); + + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 })); + Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadNextPair_ReadsAllPairs(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=&baz=2"); + + var reader = new FormReader(body); + + var pair = (KeyValuePair)await ReadPair(reader); + + Assert.Equal("foo", pair.Key); + Assert.Equal("", pair.Value); + + pair = (KeyValuePair)await ReadPair(reader); + + Assert.Equal("baz", pair.Key); + Assert.Equal("2", pair.Value); + + Assert.Null(await ReadPair(reader)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadNextPair_ReturnsNullOnEmptyStream(bool bufferRequest) + { + var body = MakeStream(bufferRequest, ""); + + var reader = new FormReader(body); + + Assert.Null(await ReadPair(reader)); + } + + // https://en.wikipedia.org/wiki/Percent-encoding + [Theory] + [InlineData("++=hello", " ", "hello")] + [InlineData("a=1+1", "a", "1 1")] + [InlineData("%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E=%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E", "\"%-.<>\\^_`{|}~", "\"%-.<>\\^_`{|}~")] + [InlineData("a=%41", "a", "A")] // ascii encoded hex + [InlineData("a=%C3%A1", "a", "\u00e1")] // utf8 code points + [InlineData("a=%u20AC", "a", "%u20AC")] // utf16 not supported + public async Task ReadForm_Decoding(string formData, string key, string expectedValue) + { + var body = MakeStream(bufferRequest: false, text: formData); + + var form = await ReadFormAsync(new FormReader(body)); + + Assert.Equal(expectedValue, form[key]); + } + + protected virtual Task> ReadFormAsync(FormReader reader) + { + return Task.FromResult(reader.ReadForm()); + } + + protected virtual Task?> ReadPair(FormReader reader) + { + return Task.FromResult(reader.ReadNextPair()); + } + + private static Stream MakeStream(bool bufferRequest, string text) + { + var formContent = Encoding.UTF8.GetBytes(text); + Stream body = new MemoryStream(formContent); + if (!bufferRequest) + { + body = new NonSeekableReadStream(body); + } + return body; + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs b/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs new file mode 100644 index 0000000000..062342fa4c --- /dev/null +++ b/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs @@ -0,0 +1,313 @@ +// 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 Moq; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + + +namespace Microsoft.AspNetCore.WebUtilities.Test +{ + public class HttpResponseStreamReaderTest + { + private static readonly char[] CharData = new char[] + { + char.MinValue, + char.MaxValue, + '\t', + ' ', + '$', + '@', + '#', + '\0', + '\v', + '\'', + '\u3190', + '\uC3A0', + 'A', + '5', + '\r', + '\uFE70', + '-', + ';', + '\r', + '\n', + 'T', + '3', + '\n', + 'K', + '\u00E6', + }; + + [Fact] + public static async Task ReadToEndAsync() + { + // Arrange + var reader = new HttpRequestStreamReader(GetLargeStream(), Encoding.UTF8); + + var result = await reader.ReadToEndAsync(); + + Assert.Equal(5000, result.Length); + } + + [Fact] + public static void TestRead() + { + // Arrange + var reader = CreateReader(); + + // Act & Assert + for (var i = 0; i < CharData.Length; i++) + { + var tmp = reader.Read(); + Assert.Equal((int)CharData[i], tmp); + } + } + + [Fact] + public static void TestPeek() + { + // Arrange + var reader = CreateReader(); + + // Act & Assert + for (var i = 0; i < CharData.Length; i++) + { + var peek = reader.Peek(); + Assert.Equal((int)CharData[i], peek); + + reader.Read(); + } + } + + [Fact] + public static void EmptyStream() + { + // Arrange + var reader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8); + var buffer = new char[10]; + + // Act + var read = reader.Read(buffer, 0, 1); + + // Assert + Assert.Equal(0, read); + } + + [Fact] + public static void Read_ReadAllCharactersAtOnce() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; + + // Act + var read = reader.Read(chars, 0, chars.Length); + + // Assert + Assert.Equal(chars.Length, read); + for (var i = 0; i < CharData.Length; i++) + { + Assert.Equal(CharData[i], chars[i]); + } + } + + [Fact] + public static async Task Read_ReadInTwoChunks() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; + + // Act + var read = await reader.ReadAsync(chars, 4, 3); + + // Assert + Assert.Equal(3, read); + for (var i = 0; i < 3; i++) + { + Assert.Equal(CharData[i], chars[i + 4]); + } + } + + [Fact] + public static void ReadLine_ReadMultipleLines() + { + // Arrange + var reader = CreateReader(); + var valueString = new string(CharData); + + // Act & Assert + var data = reader.ReadLine(); + Assert.Equal(valueString.Substring(0, valueString.IndexOf('\r')), data); + + data = reader.ReadLine(); + Assert.Equal(valueString.Substring(valueString.IndexOf('\r') + 1, 3), data); + + data = reader.ReadLine(); + Assert.Equal(valueString.Substring(valueString.IndexOf('\n') + 1, 2), data); + + data = reader.ReadLine(); + Assert.Equal((valueString.Substring(valueString.LastIndexOf('\n') + 1)), data); + } + + [Fact] + public static void ReadLine_ReadWithNoNewlines() + { + // Arrange + var reader = CreateReader(); + var valueString = new string(CharData); + var temp = new char[10]; + + // Act + reader.Read(temp, 0, 1); + var data = reader.ReadLine(); + + // Assert + Assert.Equal(valueString.Substring(1, valueString.IndexOf('\r') - 1), data); + } + + [Fact] + public static async Task ReadLineAsync_MultipleContinuousLines() + { + // Arrange + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write("\n\n\r\r\n"); + writer.Flush(); + stream.Position = 0; + + var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); + + // Act & Assert + for (var i = 0; i < 4; i++) + { + var data = await reader.ReadLineAsync(); + Assert.Equal(string.Empty, data); + } + + var eol = await reader.ReadLineAsync(); + Assert.Null(eol); + } + + [Theory] + [MemberData(nameof(HttpRequestNullData))] + public static void NullInputsInConstructor_ExpectArgumentNullException(Stream stream, Encoding encoding, ArrayPool bytePool, ArrayPool charPool) + { + Assert.Throws(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(stream, encoding, 1, bytePool, charPool); + }); + } + + + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public static void NegativeOrZeroBufferSize_ExpectArgumentOutOfRangeException(int size) + { + Assert.Throws(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, size, ArrayPool.Shared, ArrayPool.Shared); + }); + } + + [Fact] + public static void StreamCannotRead_ExpectArgumentException() + { + var mockStream = new Mock(); + mockStream.Setup(m => m.CanRead).Returns(false); + Assert.Throws(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(mockStream.Object, Encoding.UTF8, 1, ArrayPool.Shared, ArrayPool.Shared); + }); + } + + [Theory] + [MemberData(nameof(HttpRequestDisposeData))] + public static void StreamDisposed_ExpectedObjectDisposedException(Action action) + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); + httpRequestStreamReader.Dispose(); + + Assert.Throws(() => + { + action(httpRequestStreamReader); + }); + } + + [Fact] + public static async Task StreamDisposed_ExpectObjectDisposedExceptionAsync() + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); + httpRequestStreamReader.Dispose(); + + await Assert.ThrowsAsync(() => + { + return httpRequestStreamReader.ReadAsync(new char[10], 0, 1); + }); + } + private static HttpRequestStreamReader CreateReader() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(CharData); + writer.Flush(); + stream.Position = 0; + + return new HttpRequestStreamReader(stream, Encoding.UTF8); + } + + private static MemoryStream GetSmallStream() + { + var testData = new byte[] { 72, 69, 76, 76, 79 }; + return new MemoryStream(testData); + } + + private static MemoryStream GetLargeStream() + { + var testData = new byte[] { 72, 69, 76, 76, 79 }; + // System.Collections.Generic. + + var data = new List(); + for (var i = 0; i < 1000; i++) + { + data.AddRange(testData); + } + + return new MemoryStream(data.ToArray()); + } + + public static IEnumerable HttpRequestNullData() + { + yield return new object[] { null, Encoding.UTF8, ArrayPool.Shared, ArrayPool.Shared }; + yield return new object[] { new MemoryStream(), null, ArrayPool.Shared, ArrayPool.Shared }; + yield return new object[] { new MemoryStream(), Encoding.UTF8, null, ArrayPool.Shared }; + yield return new object[] { new MemoryStream(), Encoding.UTF8, ArrayPool.Shared, null }; + } + + public static IEnumerable HttpRequestDisposeData() + { + yield return new object[] { new Action((httpRequestStreamReader) => + { + var res = httpRequestStreamReader.Read(); + })}; + yield return new object[] { new Action((httpRequestStreamReader) => + { + var res = httpRequestStreamReader.Read(new char[10], 0, 1); + })}; + + yield return new object[] { new Action((httpRequestStreamReader) => + { + var res = httpRequestStreamReader.Peek(); + })}; + + } + } +} diff --git a/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs b/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs new file mode 100644 index 0000000000..7847e1384e --- /dev/null +++ b/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs @@ -0,0 +1,574 @@ +// 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 Moq; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities.Test +{ + public class HttpResponseStreamWriterTest + { + private const int DefaultCharacterChunkSize = HttpResponseStreamWriter.DefaultBufferSize; + + [Fact] + public async Task DoesNotWriteBOM() + { + // Arrange + var memoryStream = new MemoryStream(); + var encodingWithBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); + var writer = new HttpResponseStreamWriter(memoryStream, encodingWithBOM); + var expectedData = new byte[] { 97, 98, 99, 100 }; // without BOM + + // Act + using (writer) + { + await writer.WriteAsync("abcd"); + } + + // Assert + Assert.Equal(expectedData, memoryStream.ToArray()); + } + + [Fact] + public async Task DoesNotFlush_UnderlyingStream_OnDisposingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.WriteAsync("Hello"); + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + } + + [Fact] + public async Task DoesNotDispose_UnderlyingStream_OnDisposingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.WriteAsync("Hello world"); + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.DisposeCallCount); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task FlushesBuffer_OnClose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task FlushesBuffer_OnDispose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Fact] + public void NoDataWritten_Flush_DoesNotFlushUnderlyingStream() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + writer.Flush(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void FlushesBuffer_ButNotStream_OnFlush(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + writer.Write(new string('a', byteLength)); + + var expectedWriteCount = Math.Ceiling((double)byteLength / HttpResponseStreamWriter.DefaultBufferSize); + + // Act + writer.Flush(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(expectedWriteCount, stream.WriteCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Fact] + public async Task NoDataWritten_FlushAsync_DoesNotFlushUnderlyingStream() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.FlushAsync(); + + // Assert + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(0, stream.Length); + } + + [Theory] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize - 1)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize * 2)] + public async Task FlushesBuffer_ButNotStream_OnFlushAsync(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + var expectedWriteCount = Math.Ceiling((double)byteLength / HttpResponseStreamWriter.DefaultBufferSize); + + // Act + await writer.FlushAsync(); + + // Assert + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(expectedWriteCount, stream.WriteAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + public async Task FlushWriteThrows_DontFlushInDispose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream() { ThrowOnWrite = true }; + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + await writer.WriteAsync(new string('a', byteLength)); + await Assert.ThrowsAsync(() => writer.FlushAsync()); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(1, stream.WriteAsyncCallCount); + Assert.Equal(0, stream.WriteCallCount); + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(0, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void WriteChar_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + for (var i = 0; i < byteLength; i++) + { + writer.Write('a'); + } + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void WriteCharArray_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + writer.Write((new string('a', byteLength)).ToCharArray()); + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task WriteCharAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + for (var i = 0; i < byteLength; i++) + { + await writer.WriteAsync('a'); + } + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task WriteCharArrayAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + await writer.WriteAsync((new string('a', byteLength)).ToCharArray()); + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData("你好世界", "utf-16")] + [InlineData("హలో ప్రపంచ", "iso-8859-1")] + [InlineData("வணக்கம் உலக", "utf-32")] + public async Task WritesData_InExpectedEncoding(string data, string encodingName) + { + // Arrange + var encoding = Encoding.GetEncoding(encodingName); + var expectedBytes = encoding.GetBytes(data); + var stream = new MemoryStream(); + var writer = new HttpResponseStreamWriter(stream, encoding); + + // Act + using (writer) + { + await writer.WriteAsync(data); + } + + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } + + [Theory] + [InlineData('ん', 1023, "utf-8")] + [InlineData('ん', 1024, "utf-8")] + [InlineData('ん', 1050, "utf-8")] + [InlineData('你', 1023, "utf-16")] + [InlineData('你', 1024, "utf-16")] + [InlineData('你', 1050, "utf-16")] + [InlineData('హ', 1023, "iso-8859-1")] + [InlineData('హ', 1024, "iso-8859-1")] + [InlineData('హ', 1050, "iso-8859-1")] + [InlineData('வ', 1023, "utf-32")] + [InlineData('வ', 1024, "utf-32")] + [InlineData('வ', 1050, "utf-32")] + public async Task WritesData_OfDifferentLength_InExpectedEncoding( + char character, + int charCount, + string encodingName) + { + // Arrange + var encoding = Encoding.GetEncoding(encodingName); + string data = new string(character, charCount); + var expectedBytes = encoding.GetBytes(data); + var stream = new MemoryStream(); + var writer = new HttpResponseStreamWriter(stream, encoding); + + // Act + using (writer) + { + await writer.WriteAsync(data); + } + + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } + + // None of the code in HttpResponseStreamWriter differs significantly when using pooled buffers. + // + // This test effectively verifies that things are correctly constructed and disposed. Pooled buffers + // throw on the finalizer thread if not disposed, so that's why it's complicated. + [Fact] + public void HttpResponseStreamWriter_UsingPooledBuffers() + { + // Arrange + var encoding = Encoding.UTF8; + var stream = new MemoryStream(); + + var expectedBytes = encoding.GetBytes("Hello, World!"); + + using (var writer = new HttpResponseStreamWriter( + stream, + encoding, + 1024, + ArrayPool.Shared, + ArrayPool.Shared)) + { + // Act + writer.Write("Hello, World!"); + } + + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } + + [Theory] + [InlineData(DefaultCharacterChunkSize)] + [InlineData(DefaultCharacterChunkSize * 2)] + [InlineData(DefaultCharacterChunkSize * 3)] + public async Task HttpResponseStreamWriter_WritesDataCorrectly_ForCharactersHavingSurrogatePairs(int characterSize) + { + // Arrange + // Here "𐐀" (called Deseret Long I) actually represents 2 characters. Try to make this character split across + // the boundary + var content = new string('a', characterSize - 1) + "𐐀"; + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.Unicode); + + // Act + await writer.WriteAsync(content); + await writer.FlushAsync(); + + // Assert + stream.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(stream, Encoding.Unicode); + var actualContent = await streamReader.ReadToEndAsync(); + Assert.Equal(content, actualContent); + } + + [Theory] + [MemberData(nameof(HttpResponseStreamWriterData))] + public static void NullInputsInConstructor_ExpectArgumentNullException(Stream stream, Encoding encoding, ArrayPool bytePool, ArrayPool charPool) + { + Assert.Throws(() => + { + var httpRequestStreamReader = new HttpResponseStreamWriter(stream, encoding, 1, bytePool, charPool); + }); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public static void NegativeOrZeroBufferSize_ExpectArgumentOutOfRangeException(int size) + { + Assert.Throws(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, size, ArrayPool.Shared, ArrayPool.Shared); + }); + } + + [Fact] + public static void StreamCannotRead_ExpectArgumentException() + { + var mockStream = new Mock(); + mockStream.Setup(m => m.CanWrite).Returns(false); + Assert.Throws(() => + { + var httpRequestStreamReader = new HttpRequestStreamReader(mockStream.Object, Encoding.UTF8, 1, ArrayPool.Shared, ArrayPool.Shared); + }); + } + + [Theory] + [MemberData(nameof(HttpResponseDisposeData))] + public static void StreamDisposed_ExpectedObjectDisposedException(Action action) + { + var httpResponseStreamWriter = new HttpResponseStreamWriter(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); + httpResponseStreamWriter.Dispose(); + + Assert.Throws(() => + { + action(httpResponseStreamWriter); + }); + } + + [Theory] + [MemberData(nameof(HttpResponseDisposeDataAsync))] + public static async Task StreamDisposed_ExpectedObjectDisposedExceptionAsync(Func function) + { + var httpResponseStreamWriter = new HttpResponseStreamWriter(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); + httpResponseStreamWriter.Dispose(); + + await Assert.ThrowsAsync(() => + { + return function(httpResponseStreamWriter); + }); + } + + + private class TestMemoryStream : MemoryStream + { + public int FlushCallCount { get; private set; } + + public int FlushAsyncCallCount { get; private set; } + + public int CloseCallCount { get; private set; } + + public int DisposeCallCount { get; private set; } + + public int WriteCallCount { get; private set; } + + public int WriteAsyncCallCount { get; private set; } + + public bool ThrowOnWrite { get; set; } + + public override void Flush() + { + FlushCallCount++; + base.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + FlushAsyncCallCount++; + return base.FlushAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + WriteCallCount++; + if (ThrowOnWrite) + { + throw new IOException("Test IOException"); + } + base.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + WriteAsyncCallCount++; + if (ThrowOnWrite) + { + throw new IOException("Test IOException"); + } + return base.WriteAsync(buffer, offset, count, cancellationToken); + } + + protected override void Dispose(bool disposing) + { + DisposeCallCount++; + base.Dispose(disposing); + } + } + + public static IEnumerable HttpResponseStreamWriterData() + { + yield return new object[] { null, Encoding.UTF8, ArrayPool.Shared, ArrayPool.Shared }; + yield return new object[] { new MemoryStream(), null, ArrayPool.Shared, ArrayPool.Shared }; + yield return new object[] { new MemoryStream(), Encoding.UTF8, null, ArrayPool.Shared }; + yield return new object[] { new MemoryStream(), Encoding.UTF8, ArrayPool.Shared, null }; + } + + public static IEnumerable HttpResponseDisposeData() + { + yield return new object[] { new Action((httpResponseStreamWriter) => + { + httpResponseStreamWriter.Write('a'); + })}; + yield return new object[] { new Action((httpResponseStreamWriter) => + { + httpResponseStreamWriter.Write(new char[] { 'a', 'b' }, 0, 1); + })}; + + yield return new object[] { new Action((httpResponseStreamWriter) => + { + httpResponseStreamWriter.Write("hello"); + })}; + yield return new object[] { new Action((httpResponseStreamWriter) => + { + httpResponseStreamWriter.Flush(); + })}; + } + + public static IEnumerable HttpResponseDisposeDataAsync() + { + yield return new object[] { new Func(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.WriteAsync('a'); + })}; + yield return new object[] { new Func(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.WriteAsync(new char[] { 'a', 'b' }, 0, 1); + })}; + + yield return new object[] { new Func(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.WriteAsync("hello"); + })}; + yield return new object[] { new Func(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.FlushAsync(); + })}; + } + } +} diff --git a/src/Http/WebUtilities/test/Microsoft.AspNetCore.WebUtilities.Tests.csproj b/src/Http/WebUtilities/test/Microsoft.AspNetCore.WebUtilities.Tests.csproj new file mode 100644 index 0000000000..8a91421e65 --- /dev/null +++ b/src/Http/WebUtilities/test/Microsoft.AspNetCore.WebUtilities.Tests.csproj @@ -0,0 +1,11 @@ + + + + $(StandardTestTfms) + + + + + + + diff --git a/src/Http/WebUtilities/test/MultipartReaderTests.cs b/src/Http/WebUtilities/test/MultipartReaderTests.cs new file mode 100644 index 0000000000..d66ea98fed --- /dev/null +++ b/src/Http/WebUtilities/test/MultipartReaderTests.cs @@ -0,0 +1,383 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class MultipartReaderTests + { + private const string Boundary = "9051914041544843365972754266"; + // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. + private const string OnePartBody = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string OnePartBodyTwoHeaders = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"Custom-header: custom-value\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string OnePartBodyWithTrailingWhitespace = +"--9051914041544843365972754266 \r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + // It's non-compliant but common to leave off the last CRLF. + private const string OnePartBodyWithoutFinalCRLF = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--"; + private const string TwoPartBody = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string TwoPartBodyWithUnicodeFileName = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a色.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string ThreePartBody = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"\r\n" + +"Content-Type: text/html\r\n" + +"\r\n" + +"Content of a.html.\r\n" + +"\r\n" + +"--9051914041544843365972754266--\r\n"; + + private const string TwoPartBodyIncompleteBuffer = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365"; + + private static MemoryStream MakeStream(string text) + { + return new MemoryStream(Encoding.UTF8.GetBytes(text)); + } + + private static string GetString(byte[] buffer, int count) + { + return Encoding.ASCII.GetString(buffer, 0, count); + } + + [Fact] + public async Task MutipartReader_ReadSinglePartBody_Success() + { + var stream = MakeStream(OnePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_HeaderCountExceeded_Throws() + { + var stream = MakeStream(OnePartBodyTwoHeaders); + var reader = new MultipartReader(Boundary, stream) + { + HeadersCountLimit = 1, + }; + + var exception = await Assert.ThrowsAsync(() => reader.ReadNextSectionAsync()); + Assert.Equal("Multipart headers count limit 1 exceeded.", exception.Message); + } + + [Fact] + public async Task MutipartReader_HeadersLengthExceeded_Throws() + { + var stream = MakeStream(OnePartBodyTwoHeaders); + var reader = new MultipartReader(Boundary, stream) + { + HeadersLengthLimit = 60, + }; + + var exception = await Assert.ThrowsAsync(() => reader.ReadNextSectionAsync()); + Assert.Equal("Line length limit 17 exceeded.", exception.Message); + } + + [Fact] + public async Task MutipartReader_ReadSinglePartBodyWithTrailingWhitespace_Success() + { + var stream = MakeStream(OnePartBodyWithTrailingWhitespace); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadSinglePartBodyWithoutLastCRLF_Success() + { + var stream = MakeStream(OnePartBodyWithoutFinalCRLF); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadTwoPartBody_Success() + { + var stream = MakeStream(TwoPartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadTwoPartBodyWithUnicodeFileName_Success() + { + var stream = MakeStream(TwoPartBodyWithUnicodeFileName); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a色.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ThreePartBody_Success() + { + var stream = MakeStream(ThreePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file2\"; filename=\"a.html\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/html", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.html.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public void MutipartReader_BufferSizeMustBeLargerThanBoundary_Throws() + { + var stream = MakeStream(ThreePartBody); + Assert.Throws(() => + { + var reader = new MultipartReader(Boundary, stream, 5); + }); + } + + [Fact] + public async Task MutipartReader_TwoPartBodyIncompleteBuffer_TwoSectionsReadSuccessfullyThirdSectionThrows() + { + var stream = MakeStream(TwoPartBodyIncompleteBuffer); + var reader = new MultipartReader(Boundary, stream); + var buffer = new byte[128]; + + //first section can be read successfully + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var read = section.Body.Read(buffer, 0, buffer.Length); + Assert.Equal("text default", GetString(buffer, read)); + + //second section can be read successfully (even though the bottom boundary is truncated) + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + read = section.Body.Read(buffer, 0, buffer.Length); + Assert.Equal("Content of a.txt.\r\n", GetString(buffer, read)); + + await Assert.ThrowsAsync(async () => + { + // we'll be unable to ensure enough bytes are buffered to even contain a final boundary + section = await reader.ReadNextSectionAsync(); + }); + } + + [Fact] + public async Task MutipartReader_ReadInvalidUtf8Header_ReplacementCharacters() + { + var body1 = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\" filename=\"a"; + + var body2 = +".txt\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xC1, 0x21 }, 0, 2); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFD!.txt\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadInvalidUtf8SurrogateHeader_ReplacementCharacters() + { + var body1 = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\" filename=\"a"; + + var body2 = +".txt\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xED, 0xA0, 85 }, 0, 3); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFDU.txt\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/NonSeekableReadStream.cs b/src/Http/WebUtilities/test/NonSeekableReadStream.cs new file mode 100644 index 0000000000..f3c77abb38 --- /dev/null +++ b/src/Http/WebUtilities/test/NonSeekableReadStream.cs @@ -0,0 +1,74 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class NonSeekableReadStream : Stream + { + private Stream _inner; + + public NonSeekableReadStream(byte[] data) + : this(new MemoryStream(data)) + { + } + + public NonSeekableReadStream(Stream inner) + { + _inner = inner; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = Math.Max(count, 1); + return _inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + count = Math.Max(count, 1); + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + } +} diff --git a/src/Http/WebUtilities/test/QueryHelpersTests.cs b/src/Http/WebUtilities/test/QueryHelpersTests.cs new file mode 100644 index 0000000000..5607ab87aa --- /dev/null +++ b/src/Http/WebUtilities/test/QueryHelpersTests.cs @@ -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.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.WebUtilities +{ + public class QueryHelperTests + { + [Fact] + public void ParseQueryWithUniqueKeysWorks() + { + var collection = QueryHelpers.ParseQuery("?key1=value1&key2=value2"); + Assert.Equal(2, collection.Count); + Assert.Equal("value1", collection["key1"].FirstOrDefault()); + Assert.Equal("value2", collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithoutQuestionmarkWorks() + { + var collection = QueryHelpers.ParseQuery("key1=value1&key2=value2"); + Assert.Equal(2, collection.Count); + Assert.Equal("value1", collection["key1"].FirstOrDefault()); + Assert.Equal("value2", collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithDuplicateKeysGroups() + { + var collection = QueryHelpers.ParseQuery("?key1=valueA&key2=valueB&key1=valueC"); + Assert.Equal(2, collection.Count); + Assert.Equal(new[] { "valueA", "valueC" }, collection["key1"]); + Assert.Equal("valueB", collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEmptyValuesWorks() + { + var collection = QueryHelpers.ParseQuery("?key1=&key2="); + Assert.Equal(2, collection.Count); + Assert.Equal(string.Empty, collection["key1"].FirstOrDefault()); + Assert.Equal(string.Empty, collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEmptyKeyWorks() + { + var collection = QueryHelpers.ParseQuery("?=value1&="); + Assert.Single(collection); + Assert.Equal(new[] { "value1", "" }, collection[""]); + } + + [Theory] + [InlineData("http://contoso.com/", "http://contoso.com/?hello=world")] + [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?hello=world")] + [InlineData("http://contoso.com/someaction?q=test", "http://contoso.com/someaction?q=test&hello=world")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor", + "http://contoso.com/someaction?q=test&hello=world#anchor")] + [InlineData("http://contoso.com/someaction#anchor", "http://contoso.com/someaction?hello=world#anchor")] + [InlineData("http://contoso.com/#anchor", "http://contoso.com/?hello=world#anchor")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor?value", + "http://contoso.com/someaction?q=test&hello=world#anchor?value")] + [InlineData( + "http://contoso.com/someaction#anchor?stuff", + "http://contoso.com/someaction?hello=world#anchor?stuff")] + [InlineData( + "http://contoso.com/someaction?name?something", + "http://contoso.com/someaction?name?something&hello=world")] + [InlineData( + "http://contoso.com/someaction#name#something", + "http://contoso.com/someaction?hello=world#name#something")] + public void AddQueryStringWithKeyAndValue(string uri, string expectedUri) + { + var result = QueryHelpers.AddQueryString(uri, "hello", "world"); + Assert.Equal(expectedUri, result); + } + + [Theory] + [InlineData("http://contoso.com/", "http://contoso.com/?hello=world&some=text")] + [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?hello=world&some=text")] + [InlineData("http://contoso.com/someaction?q=1", "http://contoso.com/someaction?q=1&hello=world&some=text")] + [InlineData("http://contoso.com/some#action", "http://contoso.com/some?hello=world&some=text#action")] + [InlineData("http://contoso.com/some?q=1#action", "http://contoso.com/some?q=1&hello=world&some=text#action")] + [InlineData("http://contoso.com/#action", "http://contoso.com/?hello=world&some=text#action")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor?value", + "http://contoso.com/someaction?q=test&hello=world&some=text#anchor?value")] + [InlineData( + "http://contoso.com/someaction#anchor?stuff", + "http://contoso.com/someaction?hello=world&some=text#anchor?stuff")] + [InlineData( + "http://contoso.com/someaction?name?something", + "http://contoso.com/someaction?name?something&hello=world&some=text")] + [InlineData( + "http://contoso.com/someaction#name#something", + "http://contoso.com/someaction?hello=world&some=text#name#something")] + public void AddQueryStringWithDictionary(string uri, string expectedUri) + { + var queryStrings = new Dictionary() + { + { "hello", "world" }, + { "some", "text" } + }; + + var result = QueryHelpers.AddQueryString(uri, queryStrings); + Assert.Equal(expectedUri, result); + } + } +} \ No newline at end of file diff --git a/src/Http/WebUtilities/test/WebEncodersTests.cs b/src/Http/WebUtilities/test/WebEncodersTests.cs new file mode 100644 index 0000000000..bb7f71248f --- /dev/null +++ b/src/Http/WebUtilities/test/WebEncodersTests.cs @@ -0,0 +1,65 @@ +// 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.AspNetCore.WebUtilities +{ + public class WebEncodersTests + { + + [Theory] + [InlineData("", 1, 0)] + [InlineData("", 0, 1)] + [InlineData("0123456789", 9, 2)] + [InlineData("0123456789", Int32.MaxValue, 2)] + [InlineData("0123456789", 9, -1)] + public void Base64UrlDecode_BadOffsets(string input, int offset, int count) + { + // Act & assert + Assert.ThrowsAny(() => + { + var retVal = WebEncoders.Base64UrlDecode(input, offset, count); + }); + } + + [Theory] + [InlineData(0, 1, 0)] + [InlineData(0, 0, 1)] + [InlineData(10, 9, 2)] + [InlineData(10, Int32.MaxValue, 2)] + [InlineData(10, 9, -1)] + public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count) + { + // Arrange + byte[] input = new byte[inputLength]; + + // Act & assert + Assert.ThrowsAny(() => + { + var retVal = WebEncoders.Base64UrlEncode(input, offset, count); + }); + } + + [Fact] + public void DataOfVariousLengthRoundTripCorrectly() + { + for (int length = 0; length != 256; ++length) + { + var data = new byte[length]; + for (int index = 0; index != length; ++index) + { + data[index] = (byte)(5 + length + (index * 23)); + } + string text = WebEncoders.Base64UrlEncode(data); + byte[] result = WebEncoders.Base64UrlDecode(text); + + for (int index = 0; index != length; ++index) + { + Assert.Equal(data[index], result[index]); + } + } + } + } +} diff --git a/src/Http/samples/SampleApp/PooledHttpContext.cs b/src/Http/samples/SampleApp/PooledHttpContext.cs new file mode 100644 index 0000000000..58166bb572 --- /dev/null +++ b/src/Http/samples/SampleApp/PooledHttpContext.cs @@ -0,0 +1,54 @@ +// 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; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Internal; + +namespace SampleApp +{ + public class PooledHttpContext : DefaultHttpContext + { + DefaultHttpRequest _pooledHttpRequest; + DefaultHttpResponse _pooledHttpResponse; + + public PooledHttpContext(IFeatureCollection featureCollection) : + base(featureCollection) + { + } + + protected override HttpRequest InitializeHttpRequest() + { + if (_pooledHttpRequest != null) + { + _pooledHttpRequest.Initialize(this); + return _pooledHttpRequest; + } + + return new DefaultHttpRequest(this); + } + + protected override void UninitializeHttpRequest(HttpRequest instance) + { + _pooledHttpRequest = instance as DefaultHttpRequest; + _pooledHttpRequest?.Uninitialize(); + } + + protected override HttpResponse InitializeHttpResponse() + { + if (_pooledHttpResponse != null) + { + _pooledHttpResponse.Initialize(this); + return _pooledHttpResponse; + } + + return new DefaultHttpResponse(this); + } + + protected override void UninitializeHttpResponse(HttpResponse instance) + { + _pooledHttpResponse = instance as DefaultHttpResponse; + _pooledHttpResponse?.Uninitialize(); + } + } +} \ No newline at end of file diff --git a/src/Http/samples/SampleApp/PooledHttpContextFactory.cs b/src/Http/samples/SampleApp/PooledHttpContextFactory.cs new file mode 100644 index 0000000000..c61e139ac3 --- /dev/null +++ b/src/Http/samples/SampleApp/PooledHttpContextFactory.cs @@ -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 System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.ObjectPool; + +namespace SampleApp +{ + public class PooledHttpContextFactory : IHttpContextFactory + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly Stack _pool = new Stack(); + + public PooledHttpContextFactory(ObjectPoolProvider poolProvider) + : this(poolProvider, httpContextAccessor: null) + { + } + + public PooledHttpContextFactory(ObjectPoolProvider poolProvider, IHttpContextAccessor httpContextAccessor) + { + if (poolProvider == null) + { + throw new ArgumentNullException(nameof(poolProvider)); + } + + _httpContextAccessor = httpContextAccessor; + } + + public HttpContext Create(IFeatureCollection featureCollection) + { + if (featureCollection == null) + { + throw new ArgumentNullException(nameof(featureCollection)); + } + + PooledHttpContext httpContext = null; + lock (_pool) + { + if (_pool.Count != 0) + { + httpContext = _pool.Pop(); + } + } + + if (httpContext == null) + { + httpContext = new PooledHttpContext(featureCollection); + } + else + { + httpContext.Initialize(featureCollection); + } + + if (_httpContextAccessor != null) + { + _httpContextAccessor.HttpContext = httpContext; + } + return httpContext; + } + + public void Dispose(HttpContext httpContext) + { + if (_httpContextAccessor != null) + { + _httpContextAccessor.HttpContext = null; + } + + var pooled = httpContext as PooledHttpContext; + if (pooled != null) + { + pooled.Uninitialize(); + lock (_pool) + { + _pool.Push(pooled); + } + } + } + } +} diff --git a/src/Http/samples/SampleApp/Program.cs b/src/Http/samples/SampleApp/Program.cs new file mode 100644 index 0000000000..28d24befe0 --- /dev/null +++ b/src/Http/samples/SampleApp/Program.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; + +namespace SampleApp +{ + public class Program + { + public static void Main(string[] args) + { + var query = new QueryBuilder() + { + { "hello", "world" } + }.ToQueryString(); + + var uri = UriHelper.BuildAbsolute("http", new HostString("contoso.com"), query: query); + + Console.WriteLine(uri); + } + } +} diff --git a/src/Http/samples/SampleApp/SampleApp.csproj b/src/Http/samples/SampleApp/SampleApp.csproj new file mode 100644 index 0000000000..aedd176bec --- /dev/null +++ b/src/Http/samples/SampleApp/SampleApp.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp2.1;net461 + Exe + + + + + + + +