#139 - Mime multipart request parsing.
This commit is contained in:
parent
b7eb1a92bb
commit
5872feb224
|
|
@ -8,7 +8,7 @@
|
||||||
"frameworks" : {
|
"frameworks" : {
|
||||||
"aspnet50" : {
|
"aspnet50" : {
|
||||||
},
|
},
|
||||||
"aspnetcore50" : {
|
"aspnetcore50" : {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Reflection.TypeExtensions": "4.0.0-beta-*",
|
"System.Reflection.TypeExtensions": "4.0.0-beta-*",
|
||||||
"System.Runtime": "4.0.20-beta-*"
|
"System.Runtime": "4.0.20-beta-*"
|
||||||
|
|
|
||||||
|
|
@ -90,12 +90,8 @@ namespace Microsoft.AspNet.Http
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri">The Uri object</param>
|
/// <param name="uri">The Uri object</param>
|
||||||
/// <returns>The resulting FragmentString</returns>
|
/// <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);
|
string fragmentValue = uri.GetComponents(UriComponents.Fragment, UriFormat.UriEscaped);
|
||||||
if (!string.IsNullOrEmpty(fragmentValue))
|
if (!string.IsNullOrEmpty(fragmentValue))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -134,12 +134,8 @@ namespace Microsoft.AspNet.Http
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri"></param>
|
/// <param name="uri"></param>
|
||||||
/// <returns></returns>
|
/// <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(
|
return new HostString(uri.GetComponents(
|
||||||
UriComponents.NormalizedHost | // Always convert punycode to Unicode.
|
UriComponents.NormalizedHost | // Always convert punycode to Unicode.
|
||||||
UriComponents.HostAndPort, UriFormat.Unescaped));
|
UriComponents.HostAndPort, UriFormat.Unescaped));
|
||||||
|
|
|
||||||
|
|
@ -64,12 +64,6 @@ namespace Microsoft.AspNet.Http
|
||||||
/// <returns>The query value collection parsed from owin.RequestQueryString.</returns>
|
/// <returns>The query value collection parsed from owin.RequestQueryString.</returns>
|
||||||
public abstract IReadableStringCollection Query { get; }
|
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>
|
/// <summary>
|
||||||
/// Gets or set the owin.RequestProtocol.
|
/// Gets or set the owin.RequestProtocol.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -128,5 +122,21 @@ namespace Microsoft.AspNet.Http
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The owin.RequestBody Stream.</returns>
|
/// <returns>The owin.RequestBody Stream.</returns>
|
||||||
public abstract Stream Body { get; set; }
|
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.
|
// 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.
|
// 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
|
namespace Microsoft.AspNet.Http
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -8,5 +10,6 @@ namespace Microsoft.AspNet.Http
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IFormCollection : IReadableStringCollection
|
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>
|
/// </summary>
|
||||||
/// <param name="uri">The Uri object</param>
|
/// <param name="uri">The Uri object</param>
|
||||||
/// <returns>The resulting PathString</returns>
|
/// <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?
|
// REVIEW: what is the exactly correct thing to do?
|
||||||
return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped));
|
return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,8 @@ namespace Microsoft.AspNet.Http
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri">The Uri object</param>
|
/// <param name="uri">The Uri object</param>
|
||||||
/// <returns>The resulting QueryString</returns>
|
/// <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);
|
string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped);
|
||||||
if (!string.IsNullOrEmpty(queryValue))
|
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="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="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>
|
/// <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)
|
if (identity != null)
|
||||||
{
|
{
|
||||||
Identity = identity as ClaimsIdentity ?? new ClaimsIdentity(identity);
|
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
|
/// Initializes a new instance of the <see cref="AuthenticationDescription"/> class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="properties"></param>
|
/// <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;
|
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);
|
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)
|
if (arrayIndex < 0 || arrayIndex > array.Length)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, string.Empty);
|
throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, string.Empty);
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ namespace Microsoft.AspNet.Owin
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException("buffer");
|
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,7 +149,7 @@ namespace Microsoft.AspNet.Owin
|
||||||
case 0x8:
|
case 0x8:
|
||||||
return WebSocketMessageType.Close;
|
return WebSocketMessageType.Close;
|
||||||
default:
|
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:
|
case WebSocketMessageType.Close:
|
||||||
return 0x8;
|
return 0x8;
|
||||||
default:
|
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.
|
// 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.
|
// 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 System.Collections.Generic;
|
||||||
|
using Microsoft.AspNet.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.WebUtilities.Collections
|
namespace Microsoft.AspNet.PipelineCore.Collections
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the parsed form values.
|
/// Contains the parsed form values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FormCollection : ReadableStringCollection, IFormCollection
|
public class FormCollection : ReadableStringCollection, IFormCollection
|
||||||
{
|
{
|
||||||
/// <summary>
|
public FormCollection([NotNull] IDictionary<string, string[]> store)
|
||||||
/// Initializes a new instance of the <see cref="T:Microsoft.AspNet.WebUtilities.FormCollection" /> class.
|
: this(store, new FormFileCollection())
|
||||||
/// </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, [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.
|
/// Initializes a new instance of the <see cref="T:Microsoft.Owin.HeaderDictionary" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="store">The underlying data store.</param>
|
/// <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;
|
Store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNet.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.PipelineCore
|
namespace Microsoft.AspNet.PipelineCore
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.WebUtilities.Collections
|
namespace Microsoft.AspNet.PipelineCore.Collections
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Accessors for query, forms, etc.
|
/// Accessors for query, forms, etc.
|
||||||
|
|
@ -17,13 +17,8 @@ namespace Microsoft.AspNet.WebUtilities.Collections
|
||||||
/// Create a new wrapper
|
/// Create a new wrapper
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="store"></param>
|
/// <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;
|
Store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,7 +70,7 @@ namespace Microsoft.AspNet.WebUtilities.Collections
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public string Get(string key)
|
public string Get(string key)
|
||||||
{
|
{
|
||||||
return ParsingHelpers.GetJoinedValue(Store, key);
|
return GetJoinedValue(Store, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -108,5 +103,15 @@ namespace Microsoft.AspNet.WebUtilities.Collections
|
||||||
{
|
{
|
||||||
return GetEnumerator();
|
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
|
/// Create a new wrapper
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="headers"></param>
|
/// <param name="headers"></param>
|
||||||
public ResponseCookies(IHeaderDictionary headers)
|
public ResponseCookies([NotNull] IHeaderDictionary headers)
|
||||||
{
|
{
|
||||||
if (headers == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("headers");
|
|
||||||
}
|
|
||||||
|
|
||||||
Headers = headers;
|
Headers = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,13 +42,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
/// <param name="options"></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 domainHasValue = !string.IsNullOrEmpty(options.Domain);
|
||||||
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
|
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
|
||||||
bool expiresHasValue = options.Expires.HasValue;
|
bool expiresHasValue = options.Expires.HasValue;
|
||||||
|
|
@ -98,13 +88,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="options"></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 domainHasValue = !string.IsNullOrEmpty(options.Domain);
|
||||||
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
|
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,12 +214,8 @@ namespace Microsoft.AspNet.PipelineCore
|
||||||
return authTypeContext.Results;
|
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 handler = HttpAuthenticationFeature.Handler;
|
||||||
|
|
||||||
var authenticateContext = new AuthenticateContext(authenticationTypes);
|
var authenticateContext = new AuthenticateContext(authenticationTypes);
|
||||||
|
|
@ -238,12 +234,8 @@ namespace Microsoft.AspNet.PipelineCore
|
||||||
return authenticateContext.Results;
|
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 handler = HttpAuthenticationFeature.Handler;
|
||||||
|
|
||||||
var authenticateContext = new AuthenticateContext(authenticationTypes);
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.FeatureModel;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
using Microsoft.AspNet.Http.Infrastructure;
|
using Microsoft.AspNet.Http.Infrastructure;
|
||||||
using Microsoft.AspNet.FeatureModel;
|
|
||||||
using Microsoft.AspNet.HttpFeature;
|
using Microsoft.AspNet.HttpFeature;
|
||||||
using Microsoft.AspNet.PipelineCore.Collections;
|
using Microsoft.AspNet.PipelineCore.Collections;
|
||||||
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
||||||
|
|
@ -55,7 +54,7 @@ namespace Microsoft.AspNet.PipelineCore
|
||||||
|
|
||||||
private IFormFeature FormFeature
|
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
|
private IRequestCookiesFeature RequestCookiesFeature
|
||||||
|
|
@ -83,7 +82,7 @@ namespace Microsoft.AspNet.PipelineCore
|
||||||
set { HttpRequestFeature.QueryString = value.Value; }
|
set { HttpRequestFeature.QueryString = value.Value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public override long? ContentLength
|
public override long? ContentLength
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
|
@ -129,11 +128,6 @@ namespace Microsoft.AspNet.PipelineCore
|
||||||
get { return QueryFeature.Query; }
|
get { return QueryFeature.Query; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken = default(CancellationToken))
|
|
||||||
{
|
|
||||||
return FormFeature.GetFormAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string Protocol
|
public override string Protocol
|
||||||
{
|
{
|
||||||
get { return HttpRequestFeature.Protocol; }
|
get { return HttpRequestFeature.Protocol; }
|
||||||
|
|
@ -167,5 +161,21 @@ namespace Microsoft.AspNet.PipelineCore
|
||||||
get { return Headers[Constants.Headers.AcceptCharset]; }
|
get { return Headers[Constants.Headers.AcceptCharset]; }
|
||||||
set { Headers[Constants.Headers.AcceptCharset] = value; }
|
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);
|
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;
|
HttpResponseFeature.StatusCode = 401;
|
||||||
var handler = HttpAuthenticationFeature.Handler;
|
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 handler = HttpAuthenticationFeature.Handler;
|
||||||
|
|
||||||
var signInContext = new SignInContext(identities, properties == null ? null : properties.Dictionary);
|
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 handler = HttpAuthenticationFeature.Handler;
|
||||||
|
|
||||||
var signOutContext = new SignOutContext(authenticationTypes);
|
var signOutContext = new SignOutContext(authenticationTypes);
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,192 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.FeatureModel;
|
|
||||||
using Microsoft.AspNet.Http;
|
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;
|
||||||
using Microsoft.AspNet.WebUtilities.Collections;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.PipelineCore
|
namespace Microsoft.AspNet.PipelineCore
|
||||||
{
|
{
|
||||||
public class FormFeature : IFormFeature
|
public class FormFeature : IFormFeature
|
||||||
{
|
{
|
||||||
private readonly IFeatureCollection _features;
|
private readonly HttpRequest _request;
|
||||||
private readonly FeatureReference<IHttpRequestFeature> _request = FeatureReference<IHttpRequestFeature>.Default;
|
|
||||||
private Stream _bodyStream;
|
|
||||||
private IReadableStringCollection _form;
|
|
||||||
|
|
||||||
public FormFeature([NotNull] IDictionary<string, string[]> form)
|
public FormFeature([NotNull] IFormCollection form)
|
||||||
: this (new ReadableStringCollection(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;
|
get
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_features == null)
|
|
||||||
{
|
{
|
||||||
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 (!HasFormContentType)
|
||||||
|
|
||||||
if (_bodyStream == null || _bodyStream != body)
|
|
||||||
{
|
{
|
||||||
_bodyStream = body;
|
throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType);
|
||||||
if (!_bodyStream.CanSeek)
|
}
|
||||||
|
|
||||||
|
// 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();
|
// TODO: Read the charset from the content-type header after we get strongly typed headers
|
||||||
await _bodyStream.CopyToAsync(buffer, 4096, cancellationToken);
|
formFields = await FormReader.ReadFormAsync(_request.Body, cancellationToken);
|
||||||
_bodyStream = buffer;
|
|
||||||
_request.Fetch(_features).Body = _bodyStream;
|
|
||||||
_bodyStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
}
|
}
|
||||||
using (var streamReader = new StreamReader(_bodyStream, Encoding.UTF8,
|
else if (HasMultipartFormContentType())
|
||||||
detectEncodingFromByteOrderMarks: true,
|
|
||||||
bufferSize: 1024, leaveOpen: true))
|
|
||||||
{
|
{
|
||||||
string form = await streamReader.ReadToEndAsync();
|
var formAccumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
_form = FormHelpers.ParseForm(form);
|
|
||||||
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
using Microsoft.Framework.Runtime;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.PipelineCore
|
namespace Microsoft.AspNet.PipelineCore
|
||||||
{
|
{
|
||||||
public interface IFormFeature
|
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
|
#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;
|
int textLength = text.Length;
|
||||||
if (!HasValue || _count < textLength)
|
if (!HasValue || _count < textLength)
|
||||||
{
|
{
|
||||||
|
|
@ -460,12 +456,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
||||||
return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0;
|
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;
|
int textLength = text.Length;
|
||||||
if (!HasValue || _count < textLength)
|
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;
|
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;
|
int textLength = text.Length;
|
||||||
if (!HasValue || _count != textLength)
|
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;
|
string[] values;
|
||||||
return headers.TryGetValue(key, out values) ? values : null;
|
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))
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException("key");
|
throw new ArgumentNullException(nameof(key));
|
||||||
}
|
}
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
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))
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException("key");
|
throw new ArgumentNullException(nameof(key));
|
||||||
}
|
}
|
||||||
if (values == null || values.Length == 0)
|
if (values == null || values.Length == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -697,15 +673,11 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
||||||
return value;
|
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))
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException("key");
|
throw new ArgumentNullException(nameof(key));
|
||||||
}
|
}
|
||||||
if (values == null || values.Length == 0)
|
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();
|
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))
|
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)
|
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)
|
if (values == null || values.Length == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -801,12 +769,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
||||||
return values == null ? null : string.Join(",", values);
|
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;
|
string[] values;
|
||||||
return store.TryGetValue(key, out values) ? values : null;
|
return store.TryGetValue(key, out values) ? values : null;
|
||||||
}
|
}
|
||||||
|
|
@ -826,7 +790,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
||||||
// return string.IsNullOrWhiteSpace(localPort) ? localIpAddress : (localIpAddress + ":" + localPort);
|
// 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;
|
const NumberStyles styles = NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite;
|
||||||
long value;
|
long value;
|
||||||
|
|
@ -840,7 +804,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetContentLength(IHeaderDictionary headers, long? value)
|
public static void SetContentLength([NotNull] IHeaderDictionary headers, long? value)
|
||||||
{
|
{
|
||||||
if (value.HasValue)
|
if (value.HasValue)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ using System.Collections.Generic;
|
||||||
using Microsoft.AspNet.FeatureModel;
|
using Microsoft.AspNet.FeatureModel;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
using Microsoft.AspNet.HttpFeature;
|
using Microsoft.AspNet.HttpFeature;
|
||||||
|
using Microsoft.AspNet.PipelineCore.Collections;
|
||||||
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
||||||
using Microsoft.AspNet.WebUtilities;
|
using Microsoft.AspNet.WebUtilities;
|
||||||
using Microsoft.AspNet.WebUtilities.Collections;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.PipelineCore
|
namespace Microsoft.AspNet.PipelineCore
|
||||||
{
|
{
|
||||||
|
|
@ -46,7 +46,7 @@ namespace Microsoft.AspNet.PipelineCore
|
||||||
if (_query == null || _queryString != queryString)
|
if (_query == null || _queryString != queryString)
|
||||||
{
|
{
|
||||||
_queryString = queryString;
|
_queryString = queryString;
|
||||||
_query = QueryHelpers.ParseQuery(queryString);
|
_query = new ReadableStringCollection(QueryHelpers.ParseQuery(queryString));
|
||||||
}
|
}
|
||||||
return _query;
|
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.HttpFeature;
|
||||||
using Microsoft.AspNet.PipelineCore.Collections;
|
using Microsoft.AspNet.PipelineCore.Collections;
|
||||||
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
using Microsoft.AspNet.PipelineCore.Infrastructure;
|
||||||
using Microsoft.AspNet.WebUtilities.Collections;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.PipelineCore
|
namespace Microsoft.AspNet.PipelineCore
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
||||||
private List<AuthenticationResult> _results;
|
private List<AuthenticationResult> _results;
|
||||||
private List<string> _accepted;
|
private List<string> _accepted;
|
||||||
|
|
||||||
public AuthenticateContext(IEnumerable<string> authenticationTypes)
|
public AuthenticateContext([NotNull] IEnumerable<string> authenticationTypes)
|
||||||
{
|
{
|
||||||
if (authenticationTypes == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("authenticationType");
|
|
||||||
}
|
|
||||||
AuthenticationTypes = authenticationTypes;
|
AuthenticationTypes = authenticationTypes;
|
||||||
_results = new List<AuthenticationResult>();
|
_results = new List<AuthenticationResult>();
|
||||||
_accepted = new List<string>();
|
_accepted = new List<string>();
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
||||||
{
|
{
|
||||||
private List<string> _accepted;
|
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;
|
AuthenticationTypes = authenticationTypes;
|
||||||
Properties = properties ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
Properties = properties ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
_accepted = new List<string>();
|
_accepted = new List<string>();
|
||||||
|
|
@ -33,7 +29,7 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
||||||
{
|
{
|
||||||
get { return _accepted; }
|
get { return _accepted; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Accept(string authenticationType, IDictionary<string, object> description)
|
public void Accept(string authenticationType, IDictionary<string, object> description)
|
||||||
{
|
{
|
||||||
_accepted.Add(authenticationType);
|
_accepted.Add(authenticationType);
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
||||||
{
|
{
|
||||||
private List<string> _accepted;
|
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;
|
Identities = identities;
|
||||||
Properties = dictionary ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
Properties = dictionary ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
_accepted = new List<string>();
|
_accepted = new List<string>();
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
|
||||||
{
|
{
|
||||||
private List<string> _accepted;
|
private List<string> _accepted;
|
||||||
|
|
||||||
public SignOutContext(IEnumerable<string> authenticationTypes)
|
public SignOutContext([NotNull] IEnumerable<string> authenticationTypes)
|
||||||
{
|
{
|
||||||
if (authenticationTypes == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("authenticationTypes");
|
|
||||||
}
|
|
||||||
AuthenticationTypes = authenticationTypes;
|
AuthenticationTypes = authenticationTypes;
|
||||||
_accepted = new List<string>();
|
_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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNet.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.WebUtilities
|
namespace Microsoft.AspNet.WebUtilities
|
||||||
{
|
{
|
||||||
|
|
@ -50,9 +49,49 @@ namespace Microsoft.AspNet.WebUtilities
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="text">The raw query string value, with or without the leading '?'.</param>
|
/// <param name="text">The raw query string value, with or without the leading '?'.</param>
|
||||||
/// <returns>A collection of parsed keys and values.</returns>
|
/// <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": {
|
"aspnetcore50": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Diagnostics.Debug": "4.0.10-beta-*",
|
"System.Diagnostics.Debug": "4.0.10-beta-*",
|
||||||
|
"System.IO.FileSystem": "4.0.0-beta-*",
|
||||||
"System.Runtime": "4.0.20-beta-*"
|
"System.Runtime": "4.0.20-beta-*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,266 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
// 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.
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.FeatureModel;
|
using Microsoft.AspNet.WebUtilities;
|
||||||
using Microsoft.AspNet.HttpFeature;
|
|
||||||
using Moq;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.PipelineCore.Tests
|
namespace Microsoft.AspNet.PipelineCore
|
||||||
{
|
{
|
||||||
public class FormFeatureTests
|
public class FormFeatureTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetFormAsync_ReturnsParsedFormCollection()
|
public async Task ReadFormAsync_SimpleData_ReturnsParsedFormCollection()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2");
|
var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2");
|
||||||
var features = new Mock<IFeatureCollection>();
|
var context = new DefaultHttpContext();
|
||||||
var request = new Mock<IHttpRequestFeature>();
|
context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
|
||||||
request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent));
|
context.Request.Body = new MemoryStream(formContent);
|
||||||
|
|
||||||
object value = request.Object;
|
// Not cached yet
|
||||||
features.Setup(f => f.TryGetValue(typeof(IHttpRequestFeature), out value))
|
var formFeature = context.GetFeature<IFormFeature>();
|
||||||
.Returns(true);
|
Assert.Null(formFeature);
|
||||||
|
|
||||||
var provider = new FormFeature(features.Object);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var formCollection = await provider.GetFormAsync(CancellationToken.None);
|
var formCollection = await context.Request.ReadFormAsync();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("bar", formCollection["foo"]);
|
Assert.Equal("bar", formCollection["foo"]);
|
||||||
Assert.Equal("2", formCollection["baz"]);
|
Assert.Equal("2", formCollection["baz"]);
|
||||||
|
|
||||||
|
// Cached
|
||||||
|
formFeature = context.GetFeature<IFormFeature>();
|
||||||
|
Assert.NotNull(formFeature);
|
||||||
|
Assert.NotNull(formFeature.Form);
|
||||||
|
Assert.Same(formFeature.Form, formCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetFormAsync_CachesFormCollectionPerBodyStream()
|
public async Task ReadFormAsync_EmptyKeyAtEndAllowed()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var formContent1 = Encoding.UTF8.GetBytes("foo=bar&baz=2");
|
var formContent = Encoding.UTF8.GetBytes("=bar");
|
||||||
var formContent2 = Encoding.UTF8.GetBytes("collection2=value");
|
var body = new MemoryStream(formContent);
|
||||||
var features = new Mock<IFeatureCollection>();
|
|
||||||
var request = new Mock<IHttpRequestFeature>();
|
|
||||||
request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent1));
|
|
||||||
|
|
||||||
object value = request.Object;
|
var formCollection = await FormReader.ReadFormAsync(body);
|
||||||
features.Setup(f => f.TryGetValue(typeof(IHttpRequestFeature), out value))
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var provider = new FormFeature(features.Object);
|
// Assert
|
||||||
|
Assert.Equal("bar", formCollection[""].FirstOrDefault());
|
||||||
|
}
|
||||||
|
|
||||||
// Act - 1
|
[Fact]
|
||||||
var formCollection = await provider.GetFormAsync(CancellationToken.None);
|
public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formContent = Encoding.UTF8.GetBytes("=bar&baz=2");
|
||||||
|
var body = new MemoryStream(formContent);
|
||||||
|
|
||||||
// Assert - 1
|
var formCollection = await FormReader.ReadFormAsync(body);
|
||||||
Assert.Equal("bar", formCollection["foo"]);
|
|
||||||
Assert.Equal("2", formCollection["baz"]);
|
|
||||||
Assert.Same(formCollection, await provider.GetFormAsync(CancellationToken.None));
|
|
||||||
|
|
||||||
// Act - 2
|
// Assert
|
||||||
request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent2));
|
Assert.Equal("bar", formCollection[""].FirstOrDefault());
|
||||||
formCollection = await provider.GetFormAsync(CancellationToken.None);
|
Assert.Equal("2", formCollection["baz"].FirstOrDefault());
|
||||||
|
}
|
||||||
|
|
||||||
// Assert - 2
|
[Fact]
|
||||||
Assert.Equal("value", formCollection["collection2"]);
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.WebUtilities
|
namespace Microsoft.AspNet.WebUtilities
|
||||||
|
|
@ -13,8 +14,8 @@ namespace Microsoft.AspNet.WebUtilities
|
||||||
{
|
{
|
||||||
var collection = QueryHelpers.ParseQuery("?key1=value1&key2=value2");
|
var collection = QueryHelpers.ParseQuery("?key1=value1&key2=value2");
|
||||||
Assert.Equal(2, collection.Count);
|
Assert.Equal(2, collection.Count);
|
||||||
Assert.Equal("value1", collection["key1"]);
|
Assert.Equal("value1", collection["key1"].FirstOrDefault());
|
||||||
Assert.Equal("value2", collection["key2"]);
|
Assert.Equal("value2", collection["key2"].FirstOrDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -22,8 +23,8 @@ namespace Microsoft.AspNet.WebUtilities
|
||||||
{
|
{
|
||||||
var collection = QueryHelpers.ParseQuery("key1=value1&key2=value2");
|
var collection = QueryHelpers.ParseQuery("key1=value1&key2=value2");
|
||||||
Assert.Equal(2, collection.Count);
|
Assert.Equal(2, collection.Count);
|
||||||
Assert.Equal("value1", collection["key1"]);
|
Assert.Equal("value1", collection["key1"].FirstOrDefault());
|
||||||
Assert.Equal("value2", collection["key2"]);
|
Assert.Equal("value2", collection["key2"].FirstOrDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -31,8 +32,8 @@ namespace Microsoft.AspNet.WebUtilities
|
||||||
{
|
{
|
||||||
var collection = QueryHelpers.ParseQuery("?key1=valueA&key2=valueB&key1=valueC");
|
var collection = QueryHelpers.ParseQuery("?key1=valueA&key2=valueB&key1=valueC");
|
||||||
Assert.Equal(2, collection.Count);
|
Assert.Equal(2, collection.Count);
|
||||||
Assert.Equal("valueA,valueC", collection["key1"]);
|
Assert.Equal(new[] { "valueA", "valueC" }, collection["key1"]);
|
||||||
Assert.Equal("valueB", collection["key2"]);
|
Assert.Equal("valueB", collection["key2"].FirstOrDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -40,8 +41,8 @@ namespace Microsoft.AspNet.WebUtilities
|
||||||
{
|
{
|
||||||
var collection = QueryHelpers.ParseQuery("?key1=&key2=");
|
var collection = QueryHelpers.ParseQuery("?key1=&key2=");
|
||||||
Assert.Equal(2, collection.Count);
|
Assert.Equal(2, collection.Count);
|
||||||
Assert.Equal(string.Empty, collection["key1"]);
|
Assert.Equal(string.Empty, collection["key1"].FirstOrDefault());
|
||||||
Assert.Equal(string.Empty, collection["key2"]);
|
Assert.Equal(string.Empty, collection["key2"].FirstOrDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -49,7 +50,7 @@ namespace Microsoft.AspNet.WebUtilities
|
||||||
{
|
{
|
||||||
var collection = QueryHelpers.ParseQuery("?=value1&=");
|
var collection = QueryHelpers.ParseQuery("?=value1&=");
|
||||||
Assert.Equal(1, collection.Count);
|
Assert.Equal(1, collection.Count);
|
||||||
Assert.Equal("value1,", collection[""]);
|
Assert.Equal(new[] { "value1", "" }, collection[""]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue