#139 - Mime multipart request parsing.
This commit is contained in:
parent
b7eb1a92bb
commit
5872feb224
|
|
@ -8,7 +8,7 @@
|
|||
"frameworks" : {
|
||||
"aspnet50" : {
|
||||
},
|
||||
"aspnetcore50" : {
|
||||
"aspnetcore50" : {
|
||||
"dependencies": {
|
||||
"System.Reflection.TypeExtensions": "4.0.0-beta-*",
|
||||
"System.Runtime": "4.0.20-beta-*"
|
||||
|
|
|
|||
|
|
@ -90,12 +90,8 @@ namespace Microsoft.AspNet.Http
|
|||
/// </summary>
|
||||
/// <param name="uri">The Uri object</param>
|
||||
/// <returns>The resulting FragmentString</returns>
|
||||
public static FragmentString FromUriComponent(Uri uri)
|
||||
public static FragmentString FromUriComponent([NotNull] Uri uri)
|
||||
{
|
||||
if (uri == null)
|
||||
{
|
||||
throw new ArgumentNullException("uri");
|
||||
}
|
||||
string fragmentValue = uri.GetComponents(UriComponents.Fragment, UriFormat.UriEscaped);
|
||||
if (!string.IsNullOrEmpty(fragmentValue))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -134,12 +134,8 @@ namespace Microsoft.AspNet.Http
|
|||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <returns></returns>
|
||||
public static HostString FromUriComponent(Uri uri)
|
||||
public static HostString FromUriComponent([NotNull] Uri uri)
|
||||
{
|
||||
if (uri == null)
|
||||
{
|
||||
throw new ArgumentNullException("uri");
|
||||
}
|
||||
return new HostString(uri.GetComponents(
|
||||
UriComponents.NormalizedHost | // Always convert punycode to Unicode.
|
||||
UriComponents.HostAndPort, UriFormat.Unescaped));
|
||||
|
|
|
|||
|
|
@ -64,12 +64,6 @@ namespace Microsoft.AspNet.Http
|
|||
/// <returns>The query value collection parsed from owin.RequestQueryString.</returns>
|
||||
public abstract IReadableStringCollection Query { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the form collection.
|
||||
/// </summary>
|
||||
/// <returns>The form collection parsed from the request body.</returns>
|
||||
public abstract Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the owin.RequestProtocol.
|
||||
/// </summary>
|
||||
|
|
@ -128,5 +122,21 @@ namespace Microsoft.AspNet.Http
|
|||
/// </summary>
|
||||
/// <returns>The owin.RequestBody Stream.</returns>
|
||||
public abstract Stream Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks the content-type header for form types.
|
||||
/// </summary>
|
||||
public abstract bool HasFormContentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the request body as a form.
|
||||
/// </summary>
|
||||
public abstract IFormCollection Form { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reads the request body if it is a form.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -8,5 +10,6 @@ namespace Microsoft.AspNet.Http
|
|||
/// </summary>
|
||||
public interface IFormCollection : IReadableStringCollection
|
||||
{
|
||||
IFormFileCollection Files { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http
|
||||
{
|
||||
public interface IFormFile
|
||||
{
|
||||
string ContentType { get; }
|
||||
|
||||
string ContentDisposition { get; }
|
||||
|
||||
IHeaderDictionary Headers { get; }
|
||||
|
||||
long Length { get; }
|
||||
|
||||
Stream OpenReadStream();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http
|
||||
{
|
||||
public interface IFormFileCollection : IList<IFormFile>
|
||||
{
|
||||
IFormFile this[string name] { get; }
|
||||
|
||||
IFormFile GetFile(string name);
|
||||
|
||||
IList<IFormFile> GetFiles(string name);
|
||||
}
|
||||
}
|
||||
|
|
@ -86,12 +86,8 @@ namespace Microsoft.AspNet.Http
|
|||
/// </summary>
|
||||
/// <param name="uri">The Uri object</param>
|
||||
/// <returns>The resulting PathString</returns>
|
||||
public static PathString FromUriComponent(Uri uri)
|
||||
public static PathString FromUriComponent([NotNull] Uri uri)
|
||||
{
|
||||
if (uri == null)
|
||||
{
|
||||
throw new ArgumentNullException("uri");
|
||||
}
|
||||
// REVIEW: what is the exactly correct thing to do?
|
||||
return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,12 +102,8 @@ namespace Microsoft.AspNet.Http
|
|||
/// </summary>
|
||||
/// <param name="uri">The Uri object</param>
|
||||
/// <returns>The resulting QueryString</returns>
|
||||
public static QueryString FromUriComponent(Uri uri)
|
||||
public static QueryString FromUriComponent([NotNull] Uri uri)
|
||||
{
|
||||
if (uri == null)
|
||||
{
|
||||
throw new ArgumentNullException("uri");
|
||||
}
|
||||
string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped);
|
||||
if (!string.IsNullOrEmpty(queryValue))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,16 +18,8 @@ namespace Microsoft.AspNet.Http.Security
|
|||
/// <param name="identity">Assigned to Identity. May be null.</param>
|
||||
/// <param name="properties">Assigned to Properties. Contains extra information carried along with the identity.</param>
|
||||
/// <param name="description">Assigned to Description. Contains information describing the authentication provider.</param>
|
||||
public AuthenticationResult(IIdentity identity, AuthenticationProperties properties, AuthenticationDescription description)
|
||||
public AuthenticationResult(IIdentity identity, [NotNull] AuthenticationProperties properties, [NotNull] AuthenticationDescription description)
|
||||
{
|
||||
if (properties == null)
|
||||
{
|
||||
throw new ArgumentNullException("properties");
|
||||
}
|
||||
if (description == null)
|
||||
{
|
||||
throw new ArgumentNullException("description");
|
||||
}
|
||||
if (identity != null)
|
||||
{
|
||||
Identity = identity as ClaimsIdentity ?? new ClaimsIdentity(identity);
|
||||
|
|
|
|||
|
|
@ -27,12 +27,8 @@ namespace Microsoft.AspNet.Http.Security
|
|||
/// Initializes a new instance of the <see cref="AuthenticationDescription"/> class
|
||||
/// </summary>
|
||||
/// <param name="properties"></param>
|
||||
public AuthenticationDescription(IDictionary<string, object> properties)
|
||||
public AuthenticationDescription([NotNull] IDictionary<string, object> properties)
|
||||
{
|
||||
if (properties == null)
|
||||
{
|
||||
throw new ArgumentNullException("properties");
|
||||
}
|
||||
Dictionary = properties;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Owin
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
internal sealed class NotNullAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -399,12 +399,8 @@ namespace Microsoft.AspNet.Owin
|
|||
return TryGetValue(item.Key, out result) && result.Equals(item.Value);
|
||||
}
|
||||
|
||||
public void CopyTo(KeyValuePair<Type, object>[] array, int arrayIndex)
|
||||
public void CopyTo([NotNull] KeyValuePair<Type, object>[] array, int arrayIndex)
|
||||
{
|
||||
if (array == null)
|
||||
{
|
||||
throw new ArgumentNullException("array");
|
||||
}
|
||||
if (arrayIndex < 0 || arrayIndex > array.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, string.Empty);
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ namespace Microsoft.AspNet.Owin
|
|||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("buffer");
|
||||
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ namespace Microsoft.AspNet.Owin
|
|||
case 0x8:
|
||||
return WebSocketMessageType.Close;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException("messageType", messageType, string.Empty);
|
||||
throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ namespace Microsoft.AspNet.Owin
|
|||
case WebSocketMessageType.Close:
|
||||
return 0x8;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException("webSocketMessageType", webSocketMessageType, string.Empty);
|
||||
throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http;
|
||||
using Microsoft.AspNet.WebUtilities;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore
|
||||
{
|
||||
public static class BufferingHelper
|
||||
{
|
||||
internal const int DefaultBufferThreshold = 1024 * 30;
|
||||
|
||||
public static string TempDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
var temp = Environment.GetEnvironmentVariable("ASPNET_TEMP");
|
||||
if (string.IsNullOrEmpty(temp))
|
||||
{
|
||||
temp = Environment.GetEnvironmentVariable("TEMP");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(temp))
|
||||
{
|
||||
// TODO: ???
|
||||
throw new DirectoryNotFoundException(temp);
|
||||
}
|
||||
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpRequest EnableRewind([NotNull] this HttpRequest request, int bufferThreshold = DefaultBufferThreshold)
|
||||
{
|
||||
var body = request.Body;
|
||||
if (!body.CanSeek)
|
||||
{
|
||||
// TODO: Register this buffer for disposal at the end of the request to ensure the temp file is deleted.
|
||||
// Otherwise it won't get deleted until GC closes the stream.
|
||||
request.Body = new FileBufferingReadStream(body, bufferThreshold, TempDirectory);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,27 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Http;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities.Collections
|
||||
namespace Microsoft.AspNet.PipelineCore.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the parsed form values.
|
||||
/// </summary>
|
||||
public class FormCollection : ReadableStringCollection, IFormCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="T:Microsoft.AspNet.WebUtilities.FormCollection" /> class.
|
||||
/// </summary>
|
||||
/// <param name="store">The store for the form.</param>
|
||||
public FormCollection(IDictionary<string, string[]> store)
|
||||
: base(store)
|
||||
public FormCollection([NotNull] IDictionary<string, string[]> store)
|
||||
: this(store, new FormFileCollection())
|
||||
{
|
||||
}
|
||||
|
||||
public FormCollection([NotNull] IDictionary<string, string[]> store, [NotNull] IFormFileCollection files)
|
||||
: base(store)
|
||||
{
|
||||
Files = files;
|
||||
}
|
||||
|
||||
public IFormFileCollection Files { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore.Collections
|
||||
{
|
||||
public class FormFileCollection : List<IFormFile>, IFormFileCollection
|
||||
{
|
||||
public IFormFile this[string name]
|
||||
{
|
||||
get { return GetFile(name); }
|
||||
}
|
||||
|
||||
public IFormFile GetFile(string name)
|
||||
{
|
||||
return Find(file => string.Equals(name, GetName(file.ContentDisposition)));
|
||||
}
|
||||
|
||||
public IList<IFormFile> GetFiles(string name)
|
||||
{
|
||||
return FindAll(file => string.Equals(name, GetName(file.ContentDisposition)));
|
||||
}
|
||||
|
||||
private static string GetName(string contentDisposition)
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this
|
||||
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
|
||||
var offset = contentDisposition.IndexOf("name=\"") + "name=\"".Length;
|
||||
var key = contentDisposition.Substring(offset, contentDisposition.IndexOf("\"", offset) - offset); // Remove quotes
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,13 +20,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
|
|||
/// Initializes a new instance of the <see cref="T:Microsoft.Owin.HeaderDictionary" /> class.
|
||||
/// </summary>
|
||||
/// <param name="store">The underlying data store.</param>
|
||||
public HeaderDictionary(IDictionary<string, string[]> store)
|
||||
public HeaderDictionary([NotNull] IDictionary<string, string[]> store)
|
||||
{
|
||||
if (store == null)
|
||||
{
|
||||
throw new ArgumentNullException("store");
|
||||
}
|
||||
|
||||
Store = store;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using System.Collections;
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities.Collections
|
||||
namespace Microsoft.AspNet.PipelineCore.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Accessors for query, forms, etc.
|
||||
|
|
@ -17,13 +17,8 @@ namespace Microsoft.AspNet.WebUtilities.Collections
|
|||
/// Create a new wrapper
|
||||
/// </summary>
|
||||
/// <param name="store"></param>
|
||||
public ReadableStringCollection(IDictionary<string, string[]> store)
|
||||
public ReadableStringCollection([NotNull] IDictionary<string, string[]> store)
|
||||
{
|
||||
if (store == null)
|
||||
{
|
||||
throw new ArgumentNullException("store");
|
||||
}
|
||||
|
||||
Store = store;
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +70,7 @@ namespace Microsoft.AspNet.WebUtilities.Collections
|
|||
/// <returns></returns>
|
||||
public string Get(string key)
|
||||
{
|
||||
return ParsingHelpers.GetJoinedValue(Store, key);
|
||||
return GetJoinedValue(Store, key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -108,5 +103,15 @@ namespace Microsoft.AspNet.WebUtilities.Collections
|
|||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
private static string GetJoinedValue(IDictionary<string, string[]> store, string key)
|
||||
{
|
||||
string[] values;
|
||||
if (store.TryGetValue(key, out values))
|
||||
{
|
||||
return string.Join(",", values);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,13 +19,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
|
|||
/// Create a new wrapper
|
||||
/// </summary>
|
||||
/// <param name="headers"></param>
|
||||
public ResponseCookies(IHeaderDictionary headers)
|
||||
public ResponseCookies([NotNull] IHeaderDictionary headers)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
throw new ArgumentNullException("headers");
|
||||
}
|
||||
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
|
|
@ -47,13 +42,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
|
|||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="options"></param>
|
||||
public void Append(string key, string value, CookieOptions options)
|
||||
public void Append(string key, string value, [NotNull] CookieOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException("options");
|
||||
}
|
||||
|
||||
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
|
||||
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
|
||||
bool expiresHasValue = options.Expires.HasValue;
|
||||
|
|
@ -98,13 +88,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
|
|||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="options"></param>
|
||||
public void Delete(string key, CookieOptions options)
|
||||
public void Delete(string key, [NotNull] CookieOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException("options");
|
||||
}
|
||||
|
||||
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
|
||||
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
|
||||
|
||||
|
|
|
|||
|
|
@ -214,12 +214,8 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
return authTypeContext.Results;
|
||||
}
|
||||
|
||||
public override IEnumerable<AuthenticationResult> Authenticate(IEnumerable<string> authenticationTypes)
|
||||
public override IEnumerable<AuthenticationResult> Authenticate([NotNull] IEnumerable<string> authenticationTypes)
|
||||
{
|
||||
if (authenticationTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
var handler = HttpAuthenticationFeature.Handler;
|
||||
|
||||
var authenticateContext = new AuthenticateContext(authenticationTypes);
|
||||
|
|
@ -238,12 +234,8 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
return authenticateContext.Results;
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<AuthenticationResult>> AuthenticateAsync(IEnumerable<string> authenticationTypes)
|
||||
public override async Task<IEnumerable<AuthenticationResult>> AuthenticateAsync([NotNull] IEnumerable<string> authenticationTypes)
|
||||
{
|
||||
if (authenticationTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
var handler = HttpAuthenticationFeature.Handler;
|
||||
|
||||
var authenticateContext = new AuthenticateContext(authenticationTypes);
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.FeatureModel;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Infrastructure;
|
||||
using Microsoft.AspNet.FeatureModel;
|
||||
using Microsoft.AspNet.HttpFeature;
|
||||
using Microsoft.AspNet.PipelineCore.Collections;
|
||||
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
||||
|
|
@ -55,7 +54,7 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
|
||||
private IFormFeature FormFeature
|
||||
{
|
||||
get { return _form.Fetch(_features) ?? _form.Update(_features, new FormFeature(_features)); }
|
||||
get { return _form.Fetch(_features) ?? _form.Update(_features, new FormFeature(this)); }
|
||||
}
|
||||
|
||||
private IRequestCookiesFeature RequestCookiesFeature
|
||||
|
|
@ -83,7 +82,7 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
set { HttpRequestFeature.QueryString = value.Value; }
|
||||
}
|
||||
|
||||
public override long? ContentLength
|
||||
public override long? ContentLength
|
||||
{
|
||||
get
|
||||
{
|
||||
|
|
@ -129,11 +128,6 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
get { return QueryFeature.Query; }
|
||||
}
|
||||
|
||||
public override Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return FormFeature.GetFormAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override string Protocol
|
||||
{
|
||||
get { return HttpRequestFeature.Protocol; }
|
||||
|
|
@ -167,5 +161,21 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
get { return Headers[Constants.Headers.AcceptCharset]; }
|
||||
set { Headers[Constants.Headers.AcceptCharset] = value; }
|
||||
}
|
||||
|
||||
public override bool HasFormContentType
|
||||
{
|
||||
get { return FormFeature.HasFormContentType; }
|
||||
}
|
||||
|
||||
public override IFormCollection Form
|
||||
{
|
||||
get { return FormFeature.ReadForm(); }
|
||||
set { FormFeature.Form = value; }
|
||||
}
|
||||
|
||||
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return FormFeature.ReadFormAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -129,12 +129,8 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
Headers.Set(Constants.Headers.Location, location);
|
||||
}
|
||||
|
||||
public override void Challenge(AuthenticationProperties properties, IEnumerable<string> authenticationTypes)
|
||||
public override void Challenge(AuthenticationProperties properties, [NotNull] IEnumerable<string> authenticationTypes)
|
||||
{
|
||||
if (authenticationTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
HttpResponseFeature.StatusCode = 401;
|
||||
var handler = HttpAuthenticationFeature.Handler;
|
||||
|
||||
|
|
@ -152,13 +148,8 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
}
|
||||
}
|
||||
|
||||
public override void SignIn(AuthenticationProperties properties, IEnumerable<ClaimsIdentity> identities)
|
||||
public override void SignIn(AuthenticationProperties properties, [NotNull] IEnumerable<ClaimsIdentity> identities)
|
||||
{
|
||||
if (identities == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
var handler = HttpAuthenticationFeature.Handler;
|
||||
|
||||
var signInContext = new SignInContext(identities, properties == null ? null : properties.Dictionary);
|
||||
|
|
@ -175,12 +166,8 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
}
|
||||
}
|
||||
|
||||
public override void SignOut(IEnumerable<string> authenticationTypes)
|
||||
public override void SignOut([NotNull] IEnumerable<string> authenticationTypes)
|
||||
{
|
||||
if (authenticationTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
var handler = HttpAuthenticationFeature.Handler;
|
||||
|
||||
var signOutContext = new SignOutContext(authenticationTypes);
|
||||
|
|
|
|||
|
|
@ -1,71 +1,192 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.FeatureModel;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.HttpFeature;
|
||||
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
||||
using Microsoft.AspNet.PipelineCore.Collections;
|
||||
using Microsoft.AspNet.WebUtilities;
|
||||
using Microsoft.AspNet.WebUtilities.Collections;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore
|
||||
{
|
||||
public class FormFeature : IFormFeature
|
||||
{
|
||||
private readonly IFeatureCollection _features;
|
||||
private readonly FeatureReference<IHttpRequestFeature> _request = FeatureReference<IHttpRequestFeature>.Default;
|
||||
private Stream _bodyStream;
|
||||
private IReadableStringCollection _form;
|
||||
private readonly HttpRequest _request;
|
||||
|
||||
public FormFeature([NotNull] IDictionary<string, string[]> form)
|
||||
: this (new ReadableStringCollection(form))
|
||||
public FormFeature([NotNull] IFormCollection form)
|
||||
{
|
||||
Form = form;
|
||||
}
|
||||
|
||||
public FormFeature([NotNull] IReadableStringCollection form)
|
||||
public FormFeature([NotNull] HttpRequest request)
|
||||
{
|
||||
_form = form;
|
||||
_request = request;
|
||||
}
|
||||
|
||||
public FormFeature([NotNull] IFeatureCollection features)
|
||||
public bool HasFormContentType
|
||||
{
|
||||
_features = features;
|
||||
}
|
||||
|
||||
public async Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_features == null)
|
||||
get
|
||||
{
|
||||
return _form;
|
||||
// Set directly
|
||||
if (Form != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return HasApplicationFormContentType() || HasMultipartFormContentType();
|
||||
}
|
||||
}
|
||||
|
||||
public IFormCollection Form { get; set; }
|
||||
|
||||
public IFormCollection ReadForm()
|
||||
{
|
||||
if (Form != null)
|
||||
{
|
||||
return Form;
|
||||
}
|
||||
|
||||
var body = _request.Fetch(_features).Body;
|
||||
|
||||
if (_bodyStream == null || _bodyStream != body)
|
||||
if (!HasFormContentType)
|
||||
{
|
||||
_bodyStream = body;
|
||||
if (!_bodyStream.CanSeek)
|
||||
throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType);
|
||||
}
|
||||
|
||||
// TODO: How do we prevent thread exhaustion?
|
||||
return ReadFormAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Form != null)
|
||||
{
|
||||
return Form;
|
||||
}
|
||||
|
||||
if (!HasFormContentType)
|
||||
{
|
||||
throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
_request.EnableRewind();
|
||||
|
||||
IDictionary<string, string[]> formFields = null;
|
||||
var files = new FormFileCollection();
|
||||
|
||||
// Some of these code paths use StreamReader which does not support cancellation tokens.
|
||||
using (cancellationToken.Register(_request.HttpContext.Abort))
|
||||
{
|
||||
// Check the content-type
|
||||
if (HasApplicationFormContentType())
|
||||
{
|
||||
var buffer = new MemoryStream();
|
||||
await _bodyStream.CopyToAsync(buffer, 4096, cancellationToken);
|
||||
_bodyStream = buffer;
|
||||
_request.Fetch(_features).Body = _bodyStream;
|
||||
_bodyStream.Seek(0, SeekOrigin.Begin);
|
||||
// TODO: Read the charset from the content-type header after we get strongly typed headers
|
||||
formFields = await FormReader.ReadFormAsync(_request.Body, cancellationToken);
|
||||
}
|
||||
using (var streamReader = new StreamReader(_bodyStream, Encoding.UTF8,
|
||||
detectEncodingFromByteOrderMarks: true,
|
||||
bufferSize: 1024, leaveOpen: true))
|
||||
else if (HasMultipartFormContentType())
|
||||
{
|
||||
string form = await streamReader.ReadToEndAsync();
|
||||
_form = FormHelpers.ParseForm(form);
|
||||
var formAccumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var boundary = GetBoundary(_request.ContentType);
|
||||
var multipartReader = new MultipartReader(boundary, _request.Body);
|
||||
var section = await multipartReader.ReadNextSectionAsync(cancellationToken);
|
||||
while (section != null)
|
||||
{
|
||||
var headers = new HeaderDictionary(section.Headers);
|
||||
var contentDisposition = headers["Content-Disposition"];
|
||||
if (HasFileContentDisposition(contentDisposition))
|
||||
{
|
||||
// Find the end
|
||||
await section.Body.DrainAsync(cancellationToken);
|
||||
|
||||
var file = new FormFile(_request.Body, section.BaseStreamOffset.Value, section.Body.Length)
|
||||
{
|
||||
Headers = headers,
|
||||
};
|
||||
files.Add(file);
|
||||
}
|
||||
else if (HasFormDataContentDisposition(contentDisposition))
|
||||
{
|
||||
// Content-Disposition: form-data; name="key"
|
||||
//
|
||||
// value
|
||||
|
||||
// TODO: Strongly typed headers will take care of this
|
||||
var offset = contentDisposition.IndexOf("name=") + "name=".Length;
|
||||
var key = contentDisposition.Substring(offset + 1, contentDisposition.Length - offset - 2); // Remove quotes
|
||||
|
||||
// TODO: Read the charset from the content-disposition header after we get strongly typed headers
|
||||
using (var reader = new StreamReader(section.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
|
||||
{
|
||||
var value = await reader.ReadToEndAsync();
|
||||
formAccumulator.Append(key, value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + contentDisposition);
|
||||
}
|
||||
|
||||
section = await multipartReader.ReadNextSectionAsync(cancellationToken);
|
||||
}
|
||||
|
||||
formFields = formAccumulator.GetResults();
|
||||
}
|
||||
}
|
||||
return _form;
|
||||
|
||||
Form = new FormCollection(formFields, files);
|
||||
return Form;
|
||||
}
|
||||
|
||||
private bool HasApplicationFormContentType()
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// Content-Type: application/x-www-form-urlencoded; charset=utf-8
|
||||
var contentType = _request.ContentType;
|
||||
return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private bool HasMultipartFormContentType()
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq
|
||||
var contentType = _request.ContentType;
|
||||
return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("multipart/form-data", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private bool HasFormDataContentDisposition(string contentDisposition)
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// Content-Disposition: form-data; name="key";
|
||||
return !string.IsNullOrEmpty(contentDisposition) && contentDisposition.Contains("form-data") && !contentDisposition.Contains("filename=");
|
||||
}
|
||||
|
||||
private bool HasFileContentDisposition(string contentDisposition)
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
|
||||
return !string.IsNullOrEmpty(contentDisposition) && contentDisposition.Contains("form-data") && contentDisposition.Contains("filename=");
|
||||
}
|
||||
|
||||
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq
|
||||
private static string GetBoundary(string contentType)
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// TODO: Limit the length of boundary we accept. The spec says ~70 chars.
|
||||
var elements = contentType.Split(' ');
|
||||
var element = elements.Where(entry => entry.StartsWith("boundary=")).First();
|
||||
var boundary = element.Substring("boundary=".Length);
|
||||
// Remove quotes
|
||||
if (boundary.Length >= 2 && boundary[0] == '"' && boundary[boundary.Length - 1] == '"')
|
||||
{
|
||||
boundary = boundary.Substring(1, boundary.Length - 2);
|
||||
}
|
||||
return boundary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore
|
||||
{
|
||||
public class FormFile : IFormFile
|
||||
{
|
||||
private Stream _baseStream;
|
||||
private long _baseStreamOffset;
|
||||
private long _length;
|
||||
|
||||
public FormFile(Stream baseStream, long baseStreamOffset, long length)
|
||||
{
|
||||
_baseStream = baseStream;
|
||||
_baseStreamOffset = baseStreamOffset;
|
||||
_length = length;
|
||||
}
|
||||
|
||||
public string ContentDisposition
|
||||
{
|
||||
get { return Headers["Content-Disposition"]; }
|
||||
set { Headers["Content-Disposition"] = value; }
|
||||
}
|
||||
|
||||
public string ContentType
|
||||
{
|
||||
get { return Headers["Content-Type"]; }
|
||||
set { Headers["Content-Type"] = value; }
|
||||
}
|
||||
|
||||
public IHeaderDictionary Headers { get; set; }
|
||||
|
||||
public long Length
|
||||
{
|
||||
get { return _length; }
|
||||
}
|
||||
|
||||
public Stream OpenReadStream()
|
||||
{
|
||||
return new ReferenceReadStream(_baseStream, _baseStreamOffset, _length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,33 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.Framework.Runtime;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore
|
||||
{
|
||||
public interface IFormFeature
|
||||
{
|
||||
Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// Indicates if the request has a supported form content-type.
|
||||
/// </summary>
|
||||
bool HasFormContentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The parsed form, if any.
|
||||
/// </summary>
|
||||
IFormCollection Form { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parses the request body as a form.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IFormCollection ReadForm();
|
||||
|
||||
/// <summary>
|
||||
/// Parses the request body as a form.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -445,12 +445,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
|
||||
#endregion
|
||||
|
||||
public bool StartsWith(string text, StringComparison comparisonType)
|
||||
public bool StartsWith([NotNull] string text, StringComparison comparisonType)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
throw new ArgumentNullException("text");
|
||||
}
|
||||
int textLength = text.Length;
|
||||
if (!HasValue || _count < textLength)
|
||||
{
|
||||
|
|
@ -460,12 +456,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0;
|
||||
}
|
||||
|
||||
public bool EndsWith(string text, StringComparison comparisonType)
|
||||
public bool EndsWith([NotNull] string text, StringComparison comparisonType)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
throw new ArgumentNullException("text");
|
||||
}
|
||||
int textLength = text.Length;
|
||||
if (!HasValue || _count < textLength)
|
||||
{
|
||||
|
|
@ -475,12 +467,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
return string.Compare(_buffer, _offset + _count - textLength, text, 0, textLength, comparisonType) == 0;
|
||||
}
|
||||
|
||||
public bool Equals(string text, StringComparison comparisonType)
|
||||
public bool Equals([NotNull] string text, StringComparison comparisonType)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
throw new ArgumentNullException("text");
|
||||
}
|
||||
int textLength = text.Length;
|
||||
if (!HasValue || _count != textLength)
|
||||
{
|
||||
|
|
@ -615,25 +603,17 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
public static string[] GetHeaderUnmodified(IDictionary<string, string[]> headers, string key)
|
||||
public static string[] GetHeaderUnmodified([NotNull] IDictionary<string, string[]> headers, string key)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
throw new ArgumentNullException("headers");
|
||||
}
|
||||
string[] values;
|
||||
return headers.TryGetValue(key, out values) ? values : null;
|
||||
}
|
||||
|
||||
public static void SetHeader(IDictionary<string, string[]> headers, string key, string value)
|
||||
public static void SetHeader([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, string value)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
throw new ArgumentNullException("headers");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
throw new ArgumentNullException("key");
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
|
|
@ -645,15 +625,11 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
public static void SetHeaderJoined(IDictionary<string, string[]> headers, string key, params string[] values)
|
||||
public static void SetHeaderJoined([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, params string[] values)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
throw new ArgumentNullException("headers");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
throw new ArgumentNullException("key");
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
if (values == null || values.Length == 0)
|
||||
{
|
||||
|
|
@ -697,15 +673,11 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
return value;
|
||||
}
|
||||
|
||||
public static void SetHeaderUnmodified(IDictionary<string, string[]> headers, string key, params string[] values)
|
||||
public static void SetHeaderUnmodified([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, params string[] values)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
throw new ArgumentNullException("headers");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
throw new ArgumentNullException("key");
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
if (values == null || values.Length == 0)
|
||||
{
|
||||
|
|
@ -717,16 +689,12 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
public static void SetHeaderUnmodified(IDictionary<string, string[]> headers, string key, IEnumerable<string> values)
|
||||
public static void SetHeaderUnmodified([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, [NotNull] IEnumerable<string> values)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
throw new ArgumentNullException("headers");
|
||||
}
|
||||
headers[key] = values.ToArray();
|
||||
}
|
||||
|
||||
public static void AppendHeader(IDictionary<string, string[]> headers, string key, string values)
|
||||
public static void AppendHeader([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, string values)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(values))
|
||||
{
|
||||
|
|
@ -744,7 +712,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
public static void AppendHeaderJoined(IDictionary<string, string[]> headers, string key, params string[] values)
|
||||
public static void AppendHeaderJoined([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, params string[] values)
|
||||
{
|
||||
if (values == null || values.Length == 0)
|
||||
{
|
||||
|
|
@ -762,7 +730,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
public static void AppendHeaderUnmodified(IDictionary<string, string[]> headers, string key, params string[] values)
|
||||
public static void AppendHeaderUnmodified([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, params string[] values)
|
||||
{
|
||||
if (values == null || values.Length == 0)
|
||||
{
|
||||
|
|
@ -801,12 +769,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
return values == null ? null : string.Join(",", values);
|
||||
}
|
||||
|
||||
internal static string[] GetUnmodifiedValues(IDictionary<string, string[]> store, string key)
|
||||
internal static string[] GetUnmodifiedValues([NotNull] IDictionary<string, string[]> store, string key)
|
||||
{
|
||||
if (store == null)
|
||||
{
|
||||
throw new ArgumentNullException("store");
|
||||
}
|
||||
string[] values;
|
||||
return store.TryGetValue(key, out values) ? values : null;
|
||||
}
|
||||
|
|
@ -826,7 +790,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
// return string.IsNullOrWhiteSpace(localPort) ? localIpAddress : (localIpAddress + ":" + localPort);
|
||||
//}
|
||||
|
||||
public static long? GetContentLength(IHeaderDictionary headers)
|
||||
public static long? GetContentLength([NotNull] IHeaderDictionary headers)
|
||||
{
|
||||
const NumberStyles styles = NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite;
|
||||
long value;
|
||||
|
|
@ -840,7 +804,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
return null;
|
||||
}
|
||||
|
||||
public static void SetContentLength(IHeaderDictionary headers, long? value)
|
||||
public static void SetContentLength([NotNull] IHeaderDictionary headers, long? value)
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ using System.Collections.Generic;
|
|||
using Microsoft.AspNet.FeatureModel;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.HttpFeature;
|
||||
using Microsoft.AspNet.PipelineCore.Collections;
|
||||
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
||||
using Microsoft.AspNet.WebUtilities;
|
||||
using Microsoft.AspNet.WebUtilities.Collections;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore
|
||||
{
|
||||
|
|
@ -46,7 +46,7 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
if (_query == null || _queryString != queryString)
|
||||
{
|
||||
_queryString = queryString;
|
||||
_query = QueryHelpers.ParseQuery(queryString);
|
||||
_query = new ReadableStringCollection(QueryHelpers.ParseQuery(queryString));
|
||||
}
|
||||
return _query;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.PipelineCore
|
||||
{
|
||||
/// <summary>
|
||||
/// A Stream that wraps another stream starting at a certain offset and reading for the given length.
|
||||
/// </summary>
|
||||
internal class ReferenceReadStream : Stream
|
||||
{
|
||||
private readonly Stream _inner;
|
||||
private readonly long _innerOffset;
|
||||
private readonly long _length;
|
||||
private long _position;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public ReferenceReadStream([NotNull] Stream inner, long offset, long length)
|
||||
{
|
||||
_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("value", value, "The Position must be within the length of the Stream: " + Length);
|
||||
}
|
||||
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<int> 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;
|
||||
}
|
||||
#if ASPNET50
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
VerifyPosition();
|
||||
var tcs = new TaskCompletionSource<int>(state);
|
||||
BeginRead(buffer, offset, count, callback, tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private async void BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, TaskCompletionSource<int> tcs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var read = await ReadAsync(buffer, offset, count);
|
||||
tcs.TrySetResult(read);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
|
||||
if (callback != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
callback(tcs.Task);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Suppress exceptions on background threads.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
var task = (Task<int>)asyncResult;
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
#endif
|
||||
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();
|
||||
}
|
||||
#if ASPNET50
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
#endif
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ using Microsoft.AspNet.Http.Infrastructure;
|
|||
using Microsoft.AspNet.HttpFeature;
|
||||
using Microsoft.AspNet.PipelineCore.Collections;
|
||||
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
||||
using Microsoft.AspNet.WebUtilities.Collections;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,12 +17,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
|||
private List<AuthenticationResult> _results;
|
||||
private List<string> _accepted;
|
||||
|
||||
public AuthenticateContext(IEnumerable<string> authenticationTypes)
|
||||
public AuthenticateContext([NotNull] IEnumerable<string> authenticationTypes)
|
||||
{
|
||||
if (authenticationTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException("authenticationType");
|
||||
}
|
||||
AuthenticationTypes = authenticationTypes;
|
||||
_results = new List<AuthenticationResult>();
|
||||
_accepted = new List<string>();
|
||||
|
|
|
|||
|
|
@ -14,12 +14,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
|||
{
|
||||
private List<string> _accepted;
|
||||
|
||||
public ChallengeContext(IEnumerable<string> authenticationTypes, IDictionary<string, string> properties)
|
||||
public ChallengeContext([NotNull] IEnumerable<string> authenticationTypes, IDictionary<string, string> properties)
|
||||
{
|
||||
if (authenticationTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
AuthenticationTypes = authenticationTypes;
|
||||
Properties = properties ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
_accepted = new List<string>();
|
||||
|
|
@ -33,7 +29,7 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
|||
{
|
||||
get { return _accepted; }
|
||||
}
|
||||
|
||||
|
||||
public void Accept(string authenticationType, IDictionary<string, object> description)
|
||||
{
|
||||
_accepted.Add(authenticationType);
|
||||
|
|
|
|||
|
|
@ -12,12 +12,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
|||
{
|
||||
private List<string> _accepted;
|
||||
|
||||
public SignInContext(IEnumerable<ClaimsIdentity> identities, IDictionary<string, string> dictionary)
|
||||
public SignInContext([NotNull] IEnumerable<ClaimsIdentity> identities, IDictionary<string, string> dictionary)
|
||||
{
|
||||
if (identities == null)
|
||||
{
|
||||
throw new ArgumentNullException("identities");
|
||||
}
|
||||
Identities = identities;
|
||||
Properties = dictionary ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
_accepted = new List<string>();
|
||||
|
|
|
|||
|
|
@ -11,12 +11,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
|||
{
|
||||
private List<string> _accepted;
|
||||
|
||||
public SignOutContext(IEnumerable<string> authenticationTypes)
|
||||
public SignOutContext([NotNull] IEnumerable<string> authenticationTypes)
|
||||
{
|
||||
if (authenticationTypes == null)
|
||||
{
|
||||
throw new ArgumentNullException("authenticationTypes");
|
||||
}
|
||||
AuthenticationTypes = authenticationTypes;
|
||||
_accepted = new List<string>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,396 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
{
|
||||
internal class BufferedReadStream : Stream
|
||||
{
|
||||
private const char CR = '\r';
|
||||
private const char LF = '\n';
|
||||
|
||||
private readonly Stream _inner;
|
||||
private readonly byte[] _buffer;
|
||||
private int _bufferOffset = 0;
|
||||
private int _bufferCount = 0;
|
||||
private bool _disposed;
|
||||
|
||||
public BufferedReadStream([NotNull] Stream inner, int bufferSize)
|
||||
{
|
||||
_inner = inner;
|
||||
_buffer = new byte[bufferSize];
|
||||
}
|
||||
|
||||
public ArraySegment<byte> BufferedData
|
||||
{
|
||||
get { return new ArraySegment<byte>(_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("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)
|
||||
{
|
||||
_disposed = true;
|
||||
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);
|
||||
}
|
||||
#if ASPNET50
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
return _inner.BeginWrite(buffer, offset, count, callback, state);
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
_inner.EndWrite(asyncResult);
|
||||
}
|
||||
#endif
|
||||
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<int> 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);
|
||||
}
|
||||
#if ASPNET50
|
||||
// We only anticipate using ReadAsync
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
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;
|
||||
|
||||
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>(state);
|
||||
tcs.TrySetResult(toCopy);
|
||||
if (callback != null)
|
||||
{
|
||||
callback(tcs.Task);
|
||||
}
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
return _inner.BeginRead(buffer, offset, count, callback, state);
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
Task<int> task = asyncResult as Task<int>;
|
||||
if (task != null)
|
||||
{
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
return _inner.EndRead(asyncResult);
|
||||
}
|
||||
#endif
|
||||
public bool EnsureBuffered()
|
||||
{
|
||||
if (_bufferCount > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Downshift to make room
|
||||
_bufferOffset = 0;
|
||||
_bufferCount = _inner.Read(_buffer, 0, _buffer.Length);
|
||||
return _bufferCount > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<bool> 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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public string ReadLine(int lengthLimit)
|
||||
{
|
||||
CheckDisposed();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
bool foundCR = false, foundCRLF = false;
|
||||
while (!foundCRLF && EnsureBuffered())
|
||||
{
|
||||
if (builder.Length > lengthLimit)
|
||||
{
|
||||
throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit);
|
||||
}
|
||||
ProcessLineChar(builder, ref foundCR, ref foundCRLF);
|
||||
}
|
||||
|
||||
if (foundCRLF)
|
||||
{
|
||||
return builder.ToString(0, builder.Length - 2); // Drop the CRLF
|
||||
}
|
||||
// Stream ended with no CRLF.
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public async Task<string> ReadLineAsync(int lengthLimit, CancellationToken cancellationToken)
|
||||
{
|
||||
CheckDisposed();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
bool foundCR = false, foundCRLF = false;
|
||||
while (!foundCRLF && await EnsureBufferedAsync(cancellationToken))
|
||||
{
|
||||
if (builder.Length > lengthLimit)
|
||||
{
|
||||
throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit);
|
||||
}
|
||||
|
||||
ProcessLineChar(builder, ref foundCR, ref foundCRLF);
|
||||
}
|
||||
|
||||
if (foundCRLF)
|
||||
{
|
||||
return builder.ToString(0, builder.Length - 2); // Drop the CRLF
|
||||
}
|
||||
// Stream ended with no CRLF.
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private void ProcessLineChar(StringBuilder builder, ref bool foundCR, ref bool foundCRLF)
|
||||
{
|
||||
char ch = (char)_buffer[_bufferOffset]; // TODO: Encoding enforcement
|
||||
builder.Append(ch);
|
||||
_bufferOffset++;
|
||||
_bufferCount--;
|
||||
if (ch == CR)
|
||||
{
|
||||
foundCR = true;
|
||||
}
|
||||
else if (ch == LF)
|
||||
{
|
||||
if (foundCR)
|
||||
{
|
||||
foundCRLF = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
foundCR = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<byte>(buffer, offset, count);
|
||||
if (count == 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count), "The value must be greater than zero.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.WebUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class FileBufferingReadStream : Stream
|
||||
{
|
||||
private readonly Stream _inner;
|
||||
private readonly int _memoryThreshold;
|
||||
private readonly string _tempFileDirectory;
|
||||
|
||||
private Stream _buffer = new MemoryStream(); // TODO: We could have a more efficiently expanding buffer stream.
|
||||
private bool _inMemory = true;
|
||||
private bool _completelyBuffered;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
// TODO: allow for an optional buffer size limit to prevent filling hard disks. 1gb?
|
||||
public FileBufferingReadStream([NotNull] Stream inner, int memoryThreshold, [NotNull] string tempFileDirectory)
|
||||
{
|
||||
_inner = inner;
|
||||
_memoryThreshold = memoryThreshold;
|
||||
_tempFileDirectory = tempFileDirectory;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
var fileName = Path.Combine(_tempFileDirectory, "ASPNET_" + Guid.NewGuid().ToString() + ".tmp");
|
||||
return new FileStream(fileName, 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 (_inMemory && _buffer.Length + read > _memoryThreshold)
|
||||
{
|
||||
var oldBuffer = _buffer;
|
||||
_buffer = CreateTempFile();
|
||||
_inMemory = false;
|
||||
oldBuffer.Position = 0;
|
||||
oldBuffer.CopyTo(_buffer, 1024 * 16);
|
||||
}
|
||||
|
||||
if (read > 0)
|
||||
{
|
||||
_buffer.Write(buffer, offset, read);
|
||||
}
|
||||
else
|
||||
{
|
||||
_completelyBuffered = true;
|
||||
}
|
||||
|
||||
return read;
|
||||
}
|
||||
#if ASPNET50
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var tcs = new TaskCompletionSource<int>(state);
|
||||
BeginRead(buffer, offset, count, callback, tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private async void BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, TaskCompletionSource<int> tcs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var read = await ReadAsync(buffer, offset, count);
|
||||
tcs.TrySetResult(read);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
|
||||
if (callback != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
callback(tcs.Task);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Suppress exceptions on background threads.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
var task = (Task<int>)asyncResult;
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
#endif
|
||||
public override async Task<int> 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 (_inMemory && _buffer.Length + read > _memoryThreshold)
|
||||
{
|
||||
var oldBuffer = _buffer;
|
||||
_buffer = CreateTempFile();
|
||||
_inMemory = false;
|
||||
oldBuffer.Position = 0;
|
||||
await oldBuffer.CopyToAsync(_buffer, 1024 * 16, cancellationToken);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
#if ASPNET50
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
#endif
|
||||
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 (disposing)
|
||||
{
|
||||
_buffer.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(FileBufferingReadStream));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
{
|
||||
public static class FormHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses an HTTP form body.
|
||||
/// </summary>
|
||||
/// <param name="text">The HTTP form body to parse.</param>
|
||||
/// <returns>The <see cref="T:Microsoft.Owin.IFormCollection" /> object containing the parsed HTTP form body.</returns>
|
||||
public static IFormCollection ParseForm(string text)
|
||||
{
|
||||
return ParsingHelpers.GetForm(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to read an 'application/x-www-form-urlencoded' form.
|
||||
/// </summary>
|
||||
public class FormReader
|
||||
{
|
||||
private readonly TextReader _reader;
|
||||
private readonly char[] _buffer = new char[1024];
|
||||
private readonly StringBuilder _builder = new StringBuilder();
|
||||
private int _bufferOffset;
|
||||
private int _bufferCount;
|
||||
|
||||
public FormReader([NotNull] string data)
|
||||
{
|
||||
_reader = new StringReader(data);
|
||||
}
|
||||
|
||||
// TODO: Encoding
|
||||
public FormReader([NotNull] Stream stream)
|
||||
{
|
||||
_reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true);
|
||||
}
|
||||
|
||||
// Format: key1=value1&key2=value2
|
||||
/// <summary>
|
||||
/// Reads the next key value pair from the form.
|
||||
/// For unbuffered data use the async overload instead.
|
||||
/// </summary>
|
||||
/// <returns>The next key value pair, or null when the end of the form is reached.</returns>
|
||||
public KeyValuePair<string, string>? ReadNextPair()
|
||||
{
|
||||
var key = ReadWord('=');
|
||||
if (string.IsNullOrEmpty(key) && _bufferCount == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var value = ReadWord('&');
|
||||
return new KeyValuePair<string, string>(key, value);
|
||||
}
|
||||
|
||||
// Format: key1=value1&key2=value2
|
||||
/// <summary>
|
||||
/// Asynchronously reads the next key value pair from the form.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>The next key value pair, or null when the end of the form is reached.</returns>
|
||||
public async Task<KeyValuePair<string, string>?> ReadNextPairAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var key = await ReadWordAsync('=', cancellationToken);
|
||||
if (string.IsNullOrEmpty(key) && _bufferCount == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var value = await ReadWordAsync('&', cancellationToken);
|
||||
return new KeyValuePair<string, string>(key, value);
|
||||
}
|
||||
|
||||
private string ReadWord(char seperator)
|
||||
{
|
||||
// TODO: Configurable value size limit
|
||||
while (true)
|
||||
{
|
||||
// Empty
|
||||
if (_bufferCount == 0)
|
||||
{
|
||||
Buffer();
|
||||
}
|
||||
|
||||
// End
|
||||
if (_bufferCount == 0)
|
||||
{
|
||||
return BuildWord();
|
||||
}
|
||||
|
||||
var c = _buffer[_bufferOffset++];
|
||||
_bufferCount--;
|
||||
|
||||
if (c == seperator)
|
||||
{
|
||||
return BuildWord();
|
||||
}
|
||||
_builder.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ReadWordAsync(char seperator, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Configurable value size limit
|
||||
while (true)
|
||||
{
|
||||
// Empty
|
||||
if (_bufferCount == 0)
|
||||
{
|
||||
await BufferAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// End
|
||||
if (_bufferCount == 0)
|
||||
{
|
||||
return BuildWord();
|
||||
}
|
||||
|
||||
var c = _buffer[_bufferOffset++];
|
||||
_bufferCount--;
|
||||
|
||||
if (c == seperator)
|
||||
{
|
||||
return BuildWord();
|
||||
}
|
||||
_builder.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
// '+' 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);
|
||||
}
|
||||
|
||||
private async Task BufferAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: StreamReader doesn't support cancellation?
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_bufferOffset = 0;
|
||||
_bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses text from an HTTP form body.
|
||||
/// </summary>
|
||||
/// <param name="text">The HTTP form body to parse.</param>
|
||||
/// <returns>The collection containing the parsed HTTP form body.</returns>
|
||||
public static IDictionary<string, string[]> ReadForm(string text)
|
||||
{
|
||||
var reader = new FormReader(text);
|
||||
|
||||
var accumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var pair = reader.ReadNextPair();
|
||||
while (pair.HasValue)
|
||||
{
|
||||
accumulator.Append(pair.Value.Key, pair.Value.Value);
|
||||
pair = reader.ReadNextPair();
|
||||
}
|
||||
|
||||
return accumulator.GetResults();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an HTTP form body.
|
||||
/// </summary>
|
||||
/// <param name="text">The HTTP form body to parse.</param>
|
||||
/// <returns>The collection containing the parsed HTTP form body.</returns>
|
||||
public static async Task<IDictionary<string, string[]>> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
var reader = new FormReader(stream);
|
||||
|
||||
var accumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var pair = await reader.ReadNextPairAsync(cancellationToken);
|
||||
while (pair.HasValue)
|
||||
{
|
||||
accumulator.Append(pair.Value.Key, pair.Value.Value);
|
||||
pair = await reader.ReadNextPairAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return accumulator.GetResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.WebUtilities
|
||||
{
|
||||
public class KeyValueAccumulator<TKey, TValue>
|
||||
{
|
||||
private Dictionary<TKey, List<TValue>> _accumulator;
|
||||
IEqualityComparer<TKey> _comparer;
|
||||
|
||||
public KeyValueAccumulator([NotNull] IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
_comparer = comparer;
|
||||
_accumulator = new Dictionary<TKey, List<TValue>>(comparer);
|
||||
}
|
||||
|
||||
public void Append(TKey key, TValue value)
|
||||
{
|
||||
List<TValue> values;
|
||||
if (_accumulator.TryGetValue(key, out values))
|
||||
{
|
||||
values.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_accumulator[key] = new List<TValue>(1) { value };
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<TKey, TValue[]> GetResults()
|
||||
{
|
||||
var results = new Dictionary<TKey, TValue[]>(_comparer);
|
||||
foreach (var kv in _accumulator)
|
||||
{
|
||||
results.Add(kv.Key, kv.Value.ToArray());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
{
|
||||
// https://www.ietf.org/rfc/rfc2046.txt
|
||||
public class MultipartReader
|
||||
{
|
||||
private const int DefaultBufferSize = 1024 * 4;
|
||||
|
||||
private readonly BufferedReadStream _stream;
|
||||
private readonly string _boundary;
|
||||
private MultipartReaderStream _currentStream;
|
||||
|
||||
public MultipartReader([NotNull] string boundary, [NotNull] Stream stream)
|
||||
: this(boundary, stream, DefaultBufferSize)
|
||||
{
|
||||
}
|
||||
|
||||
public MultipartReader([NotNull] string boundary, [NotNull] Stream stream, int bufferSize)
|
||||
{
|
||||
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 = boundary;
|
||||
// This stream will drain any preamble data and remove the first boundary marker.
|
||||
_currentStream = new MultipartReaderStream(_stream, _boundary, expectLeadingCrlf: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The limit for individual header lines inside a multipart section.
|
||||
/// </summary>
|
||||
public int HeaderLengthLimit { get; set; } = 1024 * 4;
|
||||
|
||||
/// <summary>
|
||||
/// The combined size limit for headers per multipart section.
|
||||
/// </summary>
|
||||
public int TotalHeaderSizeLimit { get; set; } = 1024 * 16;
|
||||
|
||||
public async Task<MultipartSection> 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(cancellationToken);
|
||||
return null;
|
||||
}
|
||||
var headers = await ReadHeadersAsync(cancellationToken);
|
||||
_currentStream = new MultipartReaderStream(_stream, _boundary);
|
||||
long? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null;
|
||||
return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset };
|
||||
}
|
||||
|
||||
private async Task<IDictionary<string, string[]>> ReadHeadersAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
int totalSize = 0;
|
||||
var accumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var line = await _stream.ReadLineAsync(HeaderLengthLimit, cancellationToken);
|
||||
while (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
totalSize += line.Length;
|
||||
if (totalSize > TotalHeaderSizeLimit)
|
||||
{
|
||||
throw new InvalidOperationException("Total header size limit exceeded: " + TotalHeaderSizeLimit);
|
||||
}
|
||||
int splitIndex = line.IndexOf(':');
|
||||
Debug.Assert(splitIndex > 0, "Invalid header line: " + line);
|
||||
if (splitIndex >= 0)
|
||||
{
|
||||
var name = line.Substring(0, splitIndex);
|
||||
var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim();
|
||||
accumulator.Append(name, value);
|
||||
}
|
||||
line = await _stream.ReadLineAsync(HeaderLengthLimit, cancellationToken);
|
||||
}
|
||||
|
||||
return accumulator.GetResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
{
|
||||
internal class MultipartReaderStream : Stream
|
||||
{
|
||||
private readonly BufferedReadStream _innerStream;
|
||||
private readonly byte[] _boundaryBytes;
|
||||
private readonly int _finalBoundaryLength;
|
||||
private readonly long _innerOffset;
|
||||
private long _position;
|
||||
private long _observedLength;
|
||||
private bool _finished;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a stream that reads until it reaches the given boundary pattern.
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="boundary"></param>
|
||||
public MultipartReaderStream([NotNull] BufferedReadStream stream, [NotNull] string boundary, bool expectLeadingCrlf = true)
|
||||
{
|
||||
_innerStream = stream;
|
||||
_innerOffset = _innerStream.CanSeek ? _innerStream.Position : 0;
|
||||
if (expectLeadingCrlf)
|
||||
{
|
||||
_boundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary);
|
||||
}
|
||||
else
|
||||
{
|
||||
_boundaryBytes = Encoding.UTF8.GetBytes("--" + boundary);
|
||||
}
|
||||
_finalBoundaryLength = _boundaryBytes.Length + 2; // Include the final '--' terminator.
|
||||
}
|
||||
|
||||
public bool FinalBoundaryFound { get; private 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("value", value, "The Position must be positive.");
|
||||
}
|
||||
if (value > _observedLength)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("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();
|
||||
}
|
||||
#if ASPNET50
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
#if ASPNET50
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<int>(state);
|
||||
InternalReadAsync(buffer, offset, size, callback, tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private async void InternalReadAsync(byte[] buffer, int offset, int size, AsyncCallback callback, TaskCompletionSource<int> tcs)
|
||||
{
|
||||
try
|
||||
{
|
||||
int read = await ReadAsync(buffer, offset, size);
|
||||
tcs.TrySetResult(read);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
|
||||
if (callback != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
callback(tcs.Task);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Suppress exceptions on background threads.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
var task = (Task<int>)asyncResult;
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
#endif
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (_finished)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
PositionInnerStream();
|
||||
if (!_innerStream.EnsureBuffered(_finalBoundaryLength))
|
||||
{
|
||||
throw new IOException("Unexpected end of stream.");
|
||||
}
|
||||
var bufferedData = _innerStream.BufferedData;
|
||||
|
||||
// scan for a boundary match, full or partial.
|
||||
int matchOffset;
|
||||
int matchCount;
|
||||
int read;
|
||||
if (SubMatch(bufferedData, _boundaryBytes, out matchOffset, out 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);
|
||||
}
|
||||
Debug.Assert(matchCount == _boundaryBytes.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.
|
||||
byte[] boundary = new byte[_boundaryBytes.Length];
|
||||
read = _innerStream.Read(boundary, 0, boundary.Length);
|
||||
Debug.Assert(read == boundary.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<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_finished)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
PositionInnerStream();
|
||||
if (!await _innerStream.EnsureBufferedAsync(_finalBoundaryLength, cancellationToken))
|
||||
{
|
||||
throw new IOException("Unexpected end of stream.");
|
||||
}
|
||||
var bufferedData = _innerStream.BufferedData;
|
||||
|
||||
// scan for a boundary match, full or partial.
|
||||
int matchOffset;
|
||||
int matchCount;
|
||||
int read;
|
||||
if (SubMatch(bufferedData, _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);
|
||||
}
|
||||
Debug.Assert(matchCount == _boundaryBytes.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.
|
||||
byte[] boundary = new byte[_boundaryBytes.Length];
|
||||
read = _innerStream.Read(boundary, 0, boundary.Length);
|
||||
Debug.Assert(read == boundary.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 segment2, or does it end with the start of segment2?
|
||||
// 1: AAAAABBBBBCCCCC
|
||||
// 2: BBBBB
|
||||
// Or:
|
||||
// 1: AAAAABBB
|
||||
// 2: BBBBB
|
||||
private static bool SubMatch(ArraySegment<byte> segment1, byte[] matchBytes, out int matchOffset, out int matchCount)
|
||||
{
|
||||
matchCount = 0;
|
||||
for (matchOffset = segment1.Offset; matchOffset < segment1.Offset + segment1.Count; matchOffset++)
|
||||
{
|
||||
int countLimit = segment1.Offset - matchOffset + segment1.Count;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
{
|
||||
public class MultipartSection
|
||||
{
|
||||
public string ContentType
|
||||
{
|
||||
get
|
||||
{
|
||||
string[] values;
|
||||
if (Headers.TryGetValue("Content-Type", out values))
|
||||
{
|
||||
return string.Join(", ", values);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string ContentDisposition
|
||||
{
|
||||
get
|
||||
{
|
||||
string[] values;
|
||||
if (Headers.TryGetValue("Content-Disposition", out values))
|
||||
{
|
||||
return string.Join(", ", values);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<string, string[]> Headers { get; set; }
|
||||
|
||||
public Stream Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The position where the body starts in the total multipart body.
|
||||
/// This may not be available if the total multipart body is not seekable.
|
||||
/// </summary>
|
||||
public long? BaseStreamOffset { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http;
|
||||
using Microsoft.AspNet.WebUtilities.Collections;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
{
|
||||
internal static class ParsingHelpers
|
||||
{
|
||||
internal static void ParseDelimited(string text, char[] delimiters, Action<string, string, object> callback, object state)
|
||||
{
|
||||
int textLength = text.Length;
|
||||
int equalIndex = text.IndexOf('=');
|
||||
if (equalIndex == -1)
|
||||
{
|
||||
equalIndex = textLength;
|
||||
}
|
||||
int scanIndex = 0;
|
||||
while (scanIndex < textLength)
|
||||
{
|
||||
int delimiterIndex = text.IndexOfAny(delimiters, scanIndex);
|
||||
if (delimiterIndex == -1)
|
||||
{
|
||||
delimiterIndex = textLength;
|
||||
}
|
||||
if (equalIndex < delimiterIndex)
|
||||
{
|
||||
while (scanIndex != equalIndex && char.IsWhiteSpace(text[scanIndex]))
|
||||
{
|
||||
++scanIndex;
|
||||
}
|
||||
string name = text.Substring(scanIndex, equalIndex - scanIndex);
|
||||
string value = text.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1);
|
||||
callback(
|
||||
Uri.UnescapeDataString(name.Replace('+', ' ')),
|
||||
Uri.UnescapeDataString(value.Replace('+', ' ')),
|
||||
state);
|
||||
equalIndex = text.IndexOf('=', delimiterIndex);
|
||||
if (equalIndex == -1)
|
||||
{
|
||||
equalIndex = textLength;
|
||||
}
|
||||
}
|
||||
scanIndex = delimiterIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Action<string, string, object> AppendItemCallback = (name, value, state) =>
|
||||
{
|
||||
var dictionary = (IDictionary<string, List<String>>)state;
|
||||
|
||||
List<string> existing;
|
||||
if (!dictionary.TryGetValue(name, out existing))
|
||||
{
|
||||
dictionary.Add(name, new List<string>(1) { value });
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Add(value);
|
||||
}
|
||||
};
|
||||
|
||||
internal static IFormCollection GetForm(string text)
|
||||
{
|
||||
IDictionary<string, string[]> form = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
|
||||
var accumulator = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
ParseDelimited(text, Ampersand, AppendItemCallback, accumulator);
|
||||
foreach (var kv in accumulator)
|
||||
{
|
||||
form.Add(kv.Key, kv.Value.ToArray());
|
||||
}
|
||||
return new FormCollection(form);
|
||||
}
|
||||
|
||||
internal static string GetJoinedValue(IDictionary<string, string[]> store, string key)
|
||||
{
|
||||
string[] values = GetUnmodifiedValues(store, key);
|
||||
return values == null ? null : string.Join(",", values);
|
||||
}
|
||||
|
||||
internal static string[] GetUnmodifiedValues(IDictionary<string, string[]> store, string key)
|
||||
{
|
||||
if (store == null)
|
||||
{
|
||||
throw new ArgumentNullException("store");
|
||||
}
|
||||
string[] values;
|
||||
return store.TryGetValue(key, out values) ? values : null;
|
||||
}
|
||||
|
||||
private static readonly char[] Ampersand = new[] { '&' };
|
||||
|
||||
internal static IReadableStringCollection GetQuery(string queryString)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(queryString) && queryString[0] == '?')
|
||||
{
|
||||
queryString = queryString.Substring(1);
|
||||
}
|
||||
var accumulator = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
ParseDelimited(queryString, Ampersand, AppendItemCallback, accumulator);
|
||||
return new ReadableStringCollection(accumulator.ToDictionary(
|
||||
item => item.Key,
|
||||
item => item.Value.ToArray(),
|
||||
StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
{
|
||||
|
|
@ -50,9 +49,49 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
/// </summary>
|
||||
/// <param name="text">The raw query string value, with or without the leading '?'.</param>
|
||||
/// <returns>A collection of parsed keys and values.</returns>
|
||||
public static IReadableStringCollection ParseQuery(string text)
|
||||
public static IDictionary<string, string[]> ParseQuery(string queryString)
|
||||
{
|
||||
return ParsingHelpers.GetQuery(text);
|
||||
if (!string.IsNullOrEmpty(queryString) && queryString[0] == '?')
|
||||
{
|
||||
queryString = queryString.Substring(1);
|
||||
}
|
||||
var accumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
int textLength = queryString.Length;
|
||||
int equalIndex = queryString.IndexOf('=');
|
||||
if (equalIndex == -1)
|
||||
{
|
||||
equalIndex = textLength;
|
||||
}
|
||||
int scanIndex = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
scanIndex = delimiterIndex + 1;
|
||||
}
|
||||
|
||||
return accumulator.GetResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.WebUtilities
|
||||
{
|
||||
public static class StreamHelperExtensions
|
||||
{
|
||||
public static async Task DrainAsync(this Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] buffer = new byte[1024];
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
while (await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken) > 0)
|
||||
{
|
||||
// Not all streams support cancellation directly.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"aspnetcore50": {
|
||||
"dependencies": {
|
||||
"System.Diagnostics.Debug": "4.0.10-beta-*",
|
||||
"System.IO.FileSystem": "4.0.0-beta-*",
|
||||
"System.Runtime": "4.0.20-beta-*"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,266 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.FeatureModel;
|
||||
using Microsoft.AspNet.HttpFeature;
|
||||
using Moq;
|
||||
using Microsoft.AspNet.WebUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore.Tests
|
||||
namespace Microsoft.AspNet.PipelineCore
|
||||
{
|
||||
public class FormFeatureTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetFormAsync_ReturnsParsedFormCollection()
|
||||
public async Task ReadFormAsync_SimpleData_ReturnsParsedFormCollection()
|
||||
{
|
||||
// Arrange
|
||||
var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2");
|
||||
var features = new Mock<IFeatureCollection>();
|
||||
var request = new Mock<IHttpRequestFeature>();
|
||||
request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent));
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
|
||||
context.Request.Body = new MemoryStream(formContent);
|
||||
|
||||
object value = request.Object;
|
||||
features.Setup(f => f.TryGetValue(typeof(IHttpRequestFeature), out value))
|
||||
.Returns(true);
|
||||
|
||||
var provider = new FormFeature(features.Object);
|
||||
// Not cached yet
|
||||
var formFeature = context.GetFeature<IFormFeature>();
|
||||
Assert.Null(formFeature);
|
||||
|
||||
// Act
|
||||
var formCollection = await provider.GetFormAsync(CancellationToken.None);
|
||||
var formCollection = await context.Request.ReadFormAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("bar", formCollection["foo"]);
|
||||
Assert.Equal("2", formCollection["baz"]);
|
||||
|
||||
// Cached
|
||||
formFeature = context.GetFeature<IFormFeature>();
|
||||
Assert.NotNull(formFeature);
|
||||
Assert.NotNull(formFeature.Form);
|
||||
Assert.Same(formFeature.Form, formCollection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFormAsync_CachesFormCollectionPerBodyStream()
|
||||
public async Task ReadFormAsync_EmptyKeyAtEndAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var formContent1 = Encoding.UTF8.GetBytes("foo=bar&baz=2");
|
||||
var formContent2 = Encoding.UTF8.GetBytes("collection2=value");
|
||||
var features = new Mock<IFeatureCollection>();
|
||||
var request = new Mock<IHttpRequestFeature>();
|
||||
request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent1));
|
||||
var formContent = Encoding.UTF8.GetBytes("=bar");
|
||||
var body = new MemoryStream(formContent);
|
||||
|
||||
object value = request.Object;
|
||||
features.Setup(f => f.TryGetValue(typeof(IHttpRequestFeature), out value))
|
||||
.Returns(true);
|
||||
var formCollection = await FormReader.ReadFormAsync(body);
|
||||
|
||||
var provider = new FormFeature(features.Object);
|
||||
// Assert
|
||||
Assert.Equal("bar", formCollection[""].FirstOrDefault());
|
||||
}
|
||||
|
||||
// Act - 1
|
||||
var formCollection = await provider.GetFormAsync(CancellationToken.None);
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var formContent = Encoding.UTF8.GetBytes("=bar&baz=2");
|
||||
var body = new MemoryStream(formContent);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Equal("bar", formCollection["foo"]);
|
||||
Assert.Equal("2", formCollection["baz"]);
|
||||
Assert.Same(formCollection, await provider.GetFormAsync(CancellationToken.None));
|
||||
var formCollection = await FormReader.ReadFormAsync(body);
|
||||
|
||||
// Act - 2
|
||||
request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent2));
|
||||
formCollection = await provider.GetFormAsync(CancellationToken.None);
|
||||
// Assert
|
||||
Assert.Equal("bar", formCollection[""].FirstOrDefault());
|
||||
Assert.Equal("2", formCollection["baz"].FirstOrDefault());
|
||||
}
|
||||
|
||||
// Assert - 2
|
||||
Assert.Equal("value", formCollection["collection2"]);
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_EmptyValuedAtEndAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var formContent = Encoding.UTF8.GetBytes("foo=");
|
||||
var body = new MemoryStream(formContent);
|
||||
|
||||
var formCollection = await FormReader.ReadFormAsync(body);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("", formCollection["foo"].FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_EmptyValuedWithAdditionalEntryAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var formContent = Encoding.UTF8.GetBytes("foo=&baz=2");
|
||||
var body = new MemoryStream(formContent);
|
||||
|
||||
var formCollection = await FormReader.ReadFormAsync(body);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("", formCollection["foo"].FirstOrDefault());
|
||||
Assert.Equal("2", formCollection["baz"].FirstOrDefault());
|
||||
}
|
||||
|
||||
private const string MultipartContentType = "multipart/form-data; boundary=WebKitFormBoundary5pDRpGheQXaM8k3T";
|
||||
private const string EmptyMultipartForm =
|
||||
@"--WebKitFormBoundary5pDRpGheQXaM8k3T--";
|
||||
private const string MultipartFormWithField =
|
||||
@"--WebKitFormBoundary5pDRpGheQXaM8k3T
|
||||
Content-Disposition: form-data; name=""description""
|
||||
|
||||
Foo
|
||||
--WebKitFormBoundary5pDRpGheQXaM8k3T--";
|
||||
private const string MultipartFormWithFile =
|
||||
@"--WebKitFormBoundary5pDRpGheQXaM8k3T
|
||||
Content-Disposition: form-data; name=""myfile1""; filename=""temp.html""
|
||||
Content-Type: text/html
|
||||
|
||||
<html><body>Hello World</body></html>
|
||||
--WebKitFormBoundary5pDRpGheQXaM8k3T--";
|
||||
private const string MultipartFormWithFieldAndFile =
|
||||
@"--WebKitFormBoundary5pDRpGheQXaM8k3T
|
||||
Content-Disposition: form-data; name=""description""
|
||||
|
||||
Foo
|
||||
--WebKitFormBoundary5pDRpGheQXaM8k3T
|
||||
Content-Disposition: form-data; name=""myfile1""; filename=""temp.html""
|
||||
Content-Type: text/html
|
||||
|
||||
<html><body>Hello World</body></html>
|
||||
--WebKitFormBoundary5pDRpGheQXaM8k3T--";
|
||||
|
||||
[Fact]
|
||||
public async Task ReadForm_EmptyMultipart_ReturnsParsedFormCollection()
|
||||
{
|
||||
var formContent = Encoding.UTF8.GetBytes(EmptyMultipartForm);
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.ContentType = MultipartContentType;
|
||||
context.Request.Body = new MemoryStream(formContent);
|
||||
|
||||
// Not cached yet
|
||||
var formFeature = context.GetFeature<IFormFeature>();
|
||||
Assert.Null(formFeature);
|
||||
|
||||
var formCollection = context.Request.Form;
|
||||
|
||||
Assert.NotNull(formCollection);
|
||||
|
||||
// Cached
|
||||
formFeature = context.GetFeature<IFormFeature>();
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadForm_MultipartWithField_ReturnsParsedFormCollection()
|
||||
{
|
||||
var formContent = Encoding.UTF8.GetBytes(MultipartFormWithField);
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.ContentType = MultipartContentType;
|
||||
context.Request.Body = new MemoryStream(formContent);
|
||||
|
||||
// Not cached yet
|
||||
var formFeature = context.GetFeature<IFormFeature>();
|
||||
Assert.Null(formFeature);
|
||||
|
||||
var formCollection = context.Request.Form;
|
||||
|
||||
Assert.NotNull(formCollection);
|
||||
|
||||
// Cached
|
||||
formFeature = context.GetFeature<IFormFeature>();
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_MultipartWithFile_ReturnsParsedFormCollection()
|
||||
{
|
||||
var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFile);
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.ContentType = MultipartContentType;
|
||||
context.Request.Body = new MemoryStream(formContent);
|
||||
|
||||
// Not cached yet
|
||||
var formFeature = context.GetFeature<IFormFeature>();
|
||||
Assert.Null(formFeature);
|
||||
|
||||
var formCollection = await context.Request.ReadFormAsync();
|
||||
|
||||
Assert.NotNull(formCollection);
|
||||
|
||||
// Cached
|
||||
formFeature = context.GetFeature<IFormFeature>();
|
||||
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("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))
|
||||
{
|
||||
var content = reader.ReadToEnd();
|
||||
Assert.Equal(content, "<html><body>Hello World</body></html>");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_MultipartWithFieldAndFile_ReturnsParsedFormCollection()
|
||||
{
|
||||
var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFieldAndFile);
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.ContentType = MultipartContentType;
|
||||
context.Request.Body = new MemoryStream(formContent);
|
||||
|
||||
// Not cached yet
|
||||
var formFeature = context.GetFeature<IFormFeature>();
|
||||
Assert.Null(formFeature);
|
||||
|
||||
var formCollection = await context.Request.ReadFormAsync();
|
||||
|
||||
Assert.NotNull(formCollection);
|
||||
|
||||
// Cached
|
||||
formFeature = context.GetFeature<IFormFeature>();
|
||||
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))
|
||||
{
|
||||
var content = reader.ReadToEnd();
|
||||
Assert.Equal(content, "<html><body>Hello World</body></html>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
{
|
||||
public class MultipartReaderTests
|
||||
{
|
||||
private const string Boundary = "9051914041544843365972754266";
|
||||
private const string OnePartBody =
|
||||
@"--9051914041544843365972754266
|
||||
Content-Disposition: form-data; name=""text""
|
||||
|
||||
text default
|
||||
--9051914041544843365972754266--
|
||||
";
|
||||
private const string OnePartBodyWithTrailingWhitespace =
|
||||
@"--9051914041544843365972754266
|
||||
Content-Disposition: form-data; name=""text""
|
||||
|
||||
text default
|
||||
--9051914041544843365972754266--
|
||||
";
|
||||
// It's non-compliant but common to leave off the last CRLF.
|
||||
private const string OnePartBodyWithoutFinalCRLF =
|
||||
@"--9051914041544843365972754266
|
||||
Content-Disposition: form-data; name=""text""
|
||||
|
||||
text default
|
||||
--9051914041544843365972754266--";
|
||||
private const string TwoPartBody =
|
||||
@"--9051914041544843365972754266
|
||||
Content-Disposition: form-data; name=""text""
|
||||
|
||||
text default
|
||||
--9051914041544843365972754266
|
||||
Content-Disposition: form-data; name=""file1""; filename=""a.txt""
|
||||
Content-Type: text/plain
|
||||
|
||||
Content of a.txt.
|
||||
|
||||
--9051914041544843365972754266--
|
||||
";
|
||||
private const string ThreePartBody =
|
||||
@"--9051914041544843365972754266
|
||||
Content-Disposition: form-data; name=""text""
|
||||
|
||||
text default
|
||||
--9051914041544843365972754266
|
||||
Content-Disposition: form-data; name=""file1""; filename=""a.txt""
|
||||
Content-Type: text/plain
|
||||
|
||||
Content of a.txt.
|
||||
|
||||
--9051914041544843365972754266
|
||||
Content-Disposition: form-data; name=""file2""; filename=""a.html""
|
||||
Content-Type: text/html
|
||||
|
||||
<!DOCTYPE html><title>Content of a.html.</title>
|
||||
|
||||
--9051914041544843365972754266--
|
||||
";
|
||||
|
||||
private static MemoryStream MakeStream(string text)
|
||||
{
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes(text));
|
||||
}
|
||||
|
||||
[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.Equal(1, section.Headers.Count);
|
||||
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_ReadSinglePartBodyWithTrailingWhitespace_Success()
|
||||
{
|
||||
var stream = MakeStream(OnePartBodyWithTrailingWhitespace);
|
||||
var reader = new MultipartReader(Boundary, stream);
|
||||
|
||||
var section = await reader.ReadNextSectionAsync();
|
||||
Assert.NotNull(section);
|
||||
Assert.Equal(1, section.Headers.Count);
|
||||
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.Equal(1, section.Headers.Count);
|
||||
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.Equal(1, section.Headers.Count);
|
||||
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.Equal(1, section.Headers.Count);
|
||||
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("<!DOCTYPE html><title>Content of a.html.</title>\r\n", Encoding.ASCII.GetString(buffer.ToArray()));
|
||||
|
||||
Assert.Null(await reader.ReadNextSectionAsync());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// 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.AspNet.WebUtilities
|
||||
|
|
@ -13,8 +14,8 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
{
|
||||
var collection = QueryHelpers.ParseQuery("?key1=value1&key2=value2");
|
||||
Assert.Equal(2, collection.Count);
|
||||
Assert.Equal("value1", collection["key1"]);
|
||||
Assert.Equal("value2", collection["key2"]);
|
||||
Assert.Equal("value1", collection["key1"].FirstOrDefault());
|
||||
Assert.Equal("value2", collection["key2"].FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -22,8 +23,8 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
{
|
||||
var collection = QueryHelpers.ParseQuery("key1=value1&key2=value2");
|
||||
Assert.Equal(2, collection.Count);
|
||||
Assert.Equal("value1", collection["key1"]);
|
||||
Assert.Equal("value2", collection["key2"]);
|
||||
Assert.Equal("value1", collection["key1"].FirstOrDefault());
|
||||
Assert.Equal("value2", collection["key2"].FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -31,8 +32,8 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
{
|
||||
var collection = QueryHelpers.ParseQuery("?key1=valueA&key2=valueB&key1=valueC");
|
||||
Assert.Equal(2, collection.Count);
|
||||
Assert.Equal("valueA,valueC", collection["key1"]);
|
||||
Assert.Equal("valueB", collection["key2"]);
|
||||
Assert.Equal(new[] { "valueA", "valueC" }, collection["key1"]);
|
||||
Assert.Equal("valueB", collection["key2"].FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -40,8 +41,8 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
{
|
||||
var collection = QueryHelpers.ParseQuery("?key1=&key2=");
|
||||
Assert.Equal(2, collection.Count);
|
||||
Assert.Equal(string.Empty, collection["key1"]);
|
||||
Assert.Equal(string.Empty, collection["key2"]);
|
||||
Assert.Equal(string.Empty, collection["key1"].FirstOrDefault());
|
||||
Assert.Equal(string.Empty, collection["key2"].FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -49,7 +50,7 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
{
|
||||
var collection = QueryHelpers.ParseQuery("?=value1&=");
|
||||
Assert.Equal(1, collection.Count);
|
||||
Assert.Equal("value1,", collection[""]);
|
||||
Assert.Equal(new[] { "value1", "" }, collection[""]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue