#139 - Mime multipart request parsing.

This commit is contained in:
Chris Ross 2014-11-04 16:31:18 -08:00
parent b7eb1a92bb
commit 5872feb224
50 changed files with 2487 additions and 419 deletions

View File

@ -8,7 +8,7 @@
"frameworks" : {
"aspnet50" : {
},
"aspnetcore50" : {
"aspnetcore50" : {
"dependencies": {
"System.Reflection.TypeExtensions": "4.0.0-beta-*",
"System.Runtime": "4.0.20-beta-*"

View File

@ -90,12 +90,8 @@ namespace Microsoft.AspNet.Http
/// </summary>
/// <param name="uri">The Uri object</param>
/// <returns>The resulting FragmentString</returns>
public static FragmentString FromUriComponent(Uri uri)
public static FragmentString FromUriComponent([NotNull] Uri uri)
{
if (uri == null)
{
throw new ArgumentNullException("uri");
}
string fragmentValue = uri.GetComponents(UriComponents.Fragment, UriFormat.UriEscaped);
if (!string.IsNullOrEmpty(fragmentValue))
{

View File

@ -134,12 +134,8 @@ namespace Microsoft.AspNet.Http
/// </summary>
/// <param name="uri"></param>
/// <returns></returns>
public static HostString FromUriComponent(Uri uri)
public static HostString FromUriComponent([NotNull] Uri uri)
{
if (uri == null)
{
throw new ArgumentNullException("uri");
}
return new HostString(uri.GetComponents(
UriComponents.NormalizedHost | // Always convert punycode to Unicode.
UriComponents.HostAndPort, UriFormat.Unescaped));

View File

@ -64,12 +64,6 @@ namespace Microsoft.AspNet.Http
/// <returns>The query value collection parsed from owin.RequestQueryString.</returns>
public abstract IReadableStringCollection Query { get; }
/// <summary>
/// Gets the form collection.
/// </summary>
/// <returns>The form collection parsed from the request body.</returns>
public abstract Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Gets or set the owin.RequestProtocol.
/// </summary>
@ -128,5 +122,21 @@ namespace Microsoft.AspNet.Http
/// </summary>
/// <returns>The owin.RequestBody Stream.</returns>
public abstract Stream Body { get; set; }
/// <summary>
/// Checks the content-type header for form types.
/// </summary>
public abstract bool HasFormContentType { get; }
/// <summary>
/// Gets or sets the request body as a form.
/// </summary>
public abstract IFormCollection Form { get; set; }
/// <summary>
/// Reads the request body if it is a form.
/// </summary>
/// <returns></returns>
public abstract Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken());
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNet.Http
{
/// <summary>
@ -8,5 +10,6 @@ namespace Microsoft.AspNet.Http
/// </summary>
public interface IFormCollection : IReadableStringCollection
{
IFormFileCollection Files { get; }
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -86,12 +86,8 @@ namespace Microsoft.AspNet.Http
/// </summary>
/// <param name="uri">The Uri object</param>
/// <returns>The resulting PathString</returns>
public static PathString FromUriComponent(Uri uri)
public static PathString FromUriComponent([NotNull] Uri uri)
{
if (uri == null)
{
throw new ArgumentNullException("uri");
}
// REVIEW: what is the exactly correct thing to do?
return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped));
}

View File

@ -102,12 +102,8 @@ namespace Microsoft.AspNet.Http
/// </summary>
/// <param name="uri">The Uri object</param>
/// <returns>The resulting QueryString</returns>
public static QueryString FromUriComponent(Uri uri)
public static QueryString FromUriComponent([NotNull] Uri uri)
{
if (uri == null)
{
throw new ArgumentNullException("uri");
}
string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped);
if (!string.IsNullOrEmpty(queryValue))
{

View File

@ -18,16 +18,8 @@ namespace Microsoft.AspNet.Http.Security
/// <param name="identity">Assigned to Identity. May be null.</param>
/// <param name="properties">Assigned to Properties. Contains extra information carried along with the identity.</param>
/// <param name="description">Assigned to Description. Contains information describing the authentication provider.</param>
public AuthenticationResult(IIdentity identity, AuthenticationProperties properties, AuthenticationDescription description)
public AuthenticationResult(IIdentity identity, [NotNull] AuthenticationProperties properties, [NotNull] AuthenticationDescription description)
{
if (properties == null)
{
throw new ArgumentNullException("properties");
}
if (description == null)
{
throw new ArgumentNullException("description");
}
if (identity != null)
{
Identity = identity as ClaimsIdentity ?? new ClaimsIdentity(identity);

View File

@ -27,12 +27,8 @@ namespace Microsoft.AspNet.Http.Security
/// Initializes a new instance of the <see cref="AuthenticationDescription"/> class
/// </summary>
/// <param name="properties"></param>
public AuthenticationDescription(IDictionary<string, object> properties)
public AuthenticationDescription([NotNull] IDictionary<string, object> properties)
{
if (properties == null)
{
throw new ArgumentNullException("properties");
}
Dictionary = properties;
}

View File

@ -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
{
}
}

View File

@ -399,12 +399,8 @@ namespace Microsoft.AspNet.Owin
return TryGetValue(item.Key, out result) && result.Equals(item.Value);
}
public void CopyTo(KeyValuePair<Type, object>[] array, int arrayIndex)
public void CopyTo([NotNull] KeyValuePair<Type, object>[] array, int arrayIndex)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (arrayIndex < 0 || arrayIndex > array.Length)
{
throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, string.Empty);

View File

@ -113,7 +113,7 @@ namespace Microsoft.AspNet.Owin
}
else
{
throw new ArgumentOutOfRangeException("buffer");
throw new ArgumentOutOfRangeException(nameof(buffer));
}
}
@ -149,7 +149,7 @@ namespace Microsoft.AspNet.Owin
case 0x8:
return WebSocketMessageType.Close;
default:
throw new ArgumentOutOfRangeException("messageType", messageType, string.Empty);
throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty);
}
}
@ -164,7 +164,7 @@ namespace Microsoft.AspNet.Owin
case WebSocketMessageType.Close:
return 0x8;
default:
throw new ArgumentOutOfRangeException("webSocketMessageType", webSocketMessageType, string.Empty);
throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty);
}
}
}

View File

@ -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;
}
}
}

View File

@ -1,23 +1,27 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Http;
using System.Collections.Generic;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.WebUtilities.Collections
namespace Microsoft.AspNet.PipelineCore.Collections
{
/// <summary>
/// Contains the parsed form values.
/// </summary>
public class FormCollection : ReadableStringCollection, IFormCollection
{
/// <summary>
/// Initializes a new instance of the <see cref="T:Microsoft.AspNet.WebUtilities.FormCollection" /> class.
/// </summary>
/// <param name="store">The store for the form.</param>
public FormCollection(IDictionary<string, string[]> store)
: base(store)
public FormCollection([NotNull] IDictionary<string, string[]> store)
: this(store, new FormFileCollection())
{
}
public FormCollection([NotNull] IDictionary<string, string[]> store, [NotNull] IFormFileCollection files)
: base(store)
{
Files = files;
}
public IFormFileCollection Files { get; private set; }
}
}

View File

@ -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;
}
}
}

View File

@ -20,13 +20,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
/// Initializes a new instance of the <see cref="T:Microsoft.Owin.HeaderDictionary" /> class.
/// </summary>
/// <param name="store">The underlying data store.</param>
public HeaderDictionary(IDictionary<string, string[]> store)
public HeaderDictionary([NotNull] IDictionary<string, string[]> store)
{
if (store == null)
{
throw new ArgumentNullException("store");
}
Store = store;
}

View File

@ -3,7 +3,6 @@
using System.Collections;
using System.Collections.Generic;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.PipelineCore
{

View File

@ -6,7 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.WebUtilities.Collections
namespace Microsoft.AspNet.PipelineCore.Collections
{
/// <summary>
/// Accessors for query, forms, etc.
@ -17,13 +17,8 @@ namespace Microsoft.AspNet.WebUtilities.Collections
/// Create a new wrapper
/// </summary>
/// <param name="store"></param>
public ReadableStringCollection(IDictionary<string, string[]> store)
public ReadableStringCollection([NotNull] IDictionary<string, string[]> store)
{
if (store == null)
{
throw new ArgumentNullException("store");
}
Store = store;
}
@ -75,7 +70,7 @@ namespace Microsoft.AspNet.WebUtilities.Collections
/// <returns></returns>
public string Get(string key)
{
return ParsingHelpers.GetJoinedValue(Store, key);
return GetJoinedValue(Store, key);
}
/// <summary>
@ -108,5 +103,15 @@ namespace Microsoft.AspNet.WebUtilities.Collections
{
return GetEnumerator();
}
private static string GetJoinedValue(IDictionary<string, string[]> store, string key)
{
string[] values;
if (store.TryGetValue(key, out values))
{
return string.Join(",", values);
}
return null;
}
}
}

View File

@ -19,13 +19,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
/// Create a new wrapper
/// </summary>
/// <param name="headers"></param>
public ResponseCookies(IHeaderDictionary headers)
public ResponseCookies([NotNull] IHeaderDictionary headers)
{
if (headers == null)
{
throw new ArgumentNullException("headers");
}
Headers = headers;
}
@ -47,13 +42,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="options"></param>
public void Append(string key, string value, CookieOptions options)
public void Append(string key, string value, [NotNull] CookieOptions options)
{
if (options == null)
{
throw new ArgumentNullException("options");
}
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
bool expiresHasValue = options.Expires.HasValue;
@ -98,13 +88,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections
/// </summary>
/// <param name="key"></param>
/// <param name="options"></param>
public void Delete(string key, CookieOptions options)
public void Delete(string key, [NotNull] CookieOptions options)
{
if (options == null)
{
throw new ArgumentNullException("options");
}
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
bool pathHasValue = !string.IsNullOrEmpty(options.Path);

View File

@ -214,12 +214,8 @@ namespace Microsoft.AspNet.PipelineCore
return authTypeContext.Results;
}
public override IEnumerable<AuthenticationResult> Authenticate(IEnumerable<string> authenticationTypes)
public override IEnumerable<AuthenticationResult> Authenticate([NotNull] IEnumerable<string> authenticationTypes)
{
if (authenticationTypes == null)
{
throw new ArgumentNullException();
}
var handler = HttpAuthenticationFeature.Handler;
var authenticateContext = new AuthenticateContext(authenticationTypes);
@ -238,12 +234,8 @@ namespace Microsoft.AspNet.PipelineCore
return authenticateContext.Results;
}
public override async Task<IEnumerable<AuthenticationResult>> AuthenticateAsync(IEnumerable<string> authenticationTypes)
public override async Task<IEnumerable<AuthenticationResult>> AuthenticateAsync([NotNull] IEnumerable<string> authenticationTypes)
{
if (authenticationTypes == null)
{
throw new ArgumentNullException();
}
var handler = HttpAuthenticationFeature.Handler;
var authenticateContext = new AuthenticateContext(authenticationTypes);

View File

@ -2,13 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Infrastructure;
using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.HttpFeature;
using Microsoft.AspNet.PipelineCore.Collections;
using Microsoft.AspNet.PipelineCore.Infrastructure;
@ -55,7 +54,7 @@ namespace Microsoft.AspNet.PipelineCore
private IFormFeature FormFeature
{
get { return _form.Fetch(_features) ?? _form.Update(_features, new FormFeature(_features)); }
get { return _form.Fetch(_features) ?? _form.Update(_features, new FormFeature(this)); }
}
private IRequestCookiesFeature RequestCookiesFeature
@ -83,7 +82,7 @@ namespace Microsoft.AspNet.PipelineCore
set { HttpRequestFeature.QueryString = value.Value; }
}
public override long? ContentLength
public override long? ContentLength
{
get
{
@ -129,11 +128,6 @@ namespace Microsoft.AspNet.PipelineCore
get { return QueryFeature.Query; }
}
public override Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return FormFeature.GetFormAsync(cancellationToken);
}
public override string Protocol
{
get { return HttpRequestFeature.Protocol; }
@ -167,5 +161,21 @@ namespace Microsoft.AspNet.PipelineCore
get { return Headers[Constants.Headers.AcceptCharset]; }
set { Headers[Constants.Headers.AcceptCharset] = value; }
}
public override bool HasFormContentType
{
get { return FormFeature.HasFormContentType; }
}
public override IFormCollection Form
{
get { return FormFeature.ReadForm(); }
set { FormFeature.Form = value; }
}
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
{
return FormFeature.ReadFormAsync(cancellationToken);
}
}
}

View File

@ -129,12 +129,8 @@ namespace Microsoft.AspNet.PipelineCore
Headers.Set(Constants.Headers.Location, location);
}
public override void Challenge(AuthenticationProperties properties, IEnumerable<string> authenticationTypes)
public override void Challenge(AuthenticationProperties properties, [NotNull] IEnumerable<string> authenticationTypes)
{
if (authenticationTypes == null)
{
throw new ArgumentNullException();
}
HttpResponseFeature.StatusCode = 401;
var handler = HttpAuthenticationFeature.Handler;
@ -152,13 +148,8 @@ namespace Microsoft.AspNet.PipelineCore
}
}
public override void SignIn(AuthenticationProperties properties, IEnumerable<ClaimsIdentity> identities)
public override void SignIn(AuthenticationProperties properties, [NotNull] IEnumerable<ClaimsIdentity> identities)
{
if (identities == null)
{
throw new ArgumentNullException();
}
var handler = HttpAuthenticationFeature.Handler;
var signInContext = new SignInContext(identities, properties == null ? null : properties.Dictionary);
@ -175,12 +166,8 @@ namespace Microsoft.AspNet.PipelineCore
}
}
public override void SignOut(IEnumerable<string> authenticationTypes)
public override void SignOut([NotNull] IEnumerable<string> authenticationTypes)
{
if (authenticationTypes == null)
{
throw new ArgumentNullException();
}
var handler = HttpAuthenticationFeature.Handler;
var signOutContext = new SignOutContext(authenticationTypes);

View File

@ -1,71 +1,192 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.HttpFeature;
using Microsoft.AspNet.PipelineCore.Infrastructure;
using Microsoft.AspNet.PipelineCore.Collections;
using Microsoft.AspNet.WebUtilities;
using Microsoft.AspNet.WebUtilities.Collections;
namespace Microsoft.AspNet.PipelineCore
{
public class FormFeature : IFormFeature
{
private readonly IFeatureCollection _features;
private readonly FeatureReference<IHttpRequestFeature> _request = FeatureReference<IHttpRequestFeature>.Default;
private Stream _bodyStream;
private IReadableStringCollection _form;
private readonly HttpRequest _request;
public FormFeature([NotNull] IDictionary<string, string[]> form)
: this (new ReadableStringCollection(form))
public FormFeature([NotNull] IFormCollection form)
{
Form = form;
}
public FormFeature([NotNull] IReadableStringCollection form)
public FormFeature([NotNull] HttpRequest request)
{
_form = form;
_request = request;
}
public FormFeature([NotNull] IFeatureCollection features)
public bool HasFormContentType
{
_features = features;
}
public async Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken)
{
if (_features == null)
get
{
return _form;
// Set directly
if (Form != null)
{
return true;
}
return HasApplicationFormContentType() || HasMultipartFormContentType();
}
}
public IFormCollection Form { get; set; }
public IFormCollection ReadForm()
{
if (Form != null)
{
return Form;
}
var body = _request.Fetch(_features).Body;
if (_bodyStream == null || _bodyStream != body)
if (!HasFormContentType)
{
_bodyStream = body;
if (!_bodyStream.CanSeek)
throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType);
}
// TODO: How do we prevent thread exhaustion?
return ReadFormAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public async Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
{
if (Form != null)
{
return Form;
}
if (!HasFormContentType)
{
throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType);
}
cancellationToken.ThrowIfCancellationRequested();
_request.EnableRewind();
IDictionary<string, string[]> formFields = null;
var files = new FormFileCollection();
// Some of these code paths use StreamReader which does not support cancellation tokens.
using (cancellationToken.Register(_request.HttpContext.Abort))
{
// Check the content-type
if (HasApplicationFormContentType())
{
var buffer = new MemoryStream();
await _bodyStream.CopyToAsync(buffer, 4096, cancellationToken);
_bodyStream = buffer;
_request.Fetch(_features).Body = _bodyStream;
_bodyStream.Seek(0, SeekOrigin.Begin);
// TODO: Read the charset from the content-type header after we get strongly typed headers
formFields = await FormReader.ReadFormAsync(_request.Body, cancellationToken);
}
using (var streamReader = new StreamReader(_bodyStream, Encoding.UTF8,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024, leaveOpen: true))
else if (HasMultipartFormContentType())
{
string form = await streamReader.ReadToEndAsync();
_form = FormHelpers.ParseForm(form);
var formAccumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
var boundary = GetBoundary(_request.ContentType);
var multipartReader = new MultipartReader(boundary, _request.Body);
var section = await multipartReader.ReadNextSectionAsync(cancellationToken);
while (section != null)
{
var headers = new HeaderDictionary(section.Headers);
var contentDisposition = headers["Content-Disposition"];
if (HasFileContentDisposition(contentDisposition))
{
// Find the end
await section.Body.DrainAsync(cancellationToken);
var file = new FormFile(_request.Body, section.BaseStreamOffset.Value, section.Body.Length)
{
Headers = headers,
};
files.Add(file);
}
else if (HasFormDataContentDisposition(contentDisposition))
{
// Content-Disposition: form-data; name="key"
//
// value
// TODO: Strongly typed headers will take care of this
var offset = contentDisposition.IndexOf("name=") + "name=".Length;
var key = contentDisposition.Substring(offset + 1, contentDisposition.Length - offset - 2); // Remove quotes
// TODO: Read the charset from the content-disposition header after we get strongly typed headers
using (var reader = new StreamReader(section.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
{
var value = await reader.ReadToEndAsync();
formAccumulator.Append(key, value);
}
}
else
{
System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + contentDisposition);
}
section = await multipartReader.ReadNextSectionAsync(cancellationToken);
}
formFields = formAccumulator.GetResults();
}
}
return _form;
Form = new FormCollection(formFields, files);
return Form;
}
private bool HasApplicationFormContentType()
{
// TODO: Strongly typed headers will take care of this for us
// Content-Type: application/x-www-form-urlencoded; charset=utf-8
var contentType = _request.ContentType;
return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) >= 0;
}
private bool HasMultipartFormContentType()
{
// TODO: Strongly typed headers will take care of this for us
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq
var contentType = _request.ContentType;
return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("multipart/form-data", StringComparison.OrdinalIgnoreCase) >= 0;
}
private bool HasFormDataContentDisposition(string contentDisposition)
{
// TODO: Strongly typed headers will take care of this for us
// Content-Disposition: form-data; name="key";
return !string.IsNullOrEmpty(contentDisposition) && contentDisposition.Contains("form-data") && !contentDisposition.Contains("filename=");
}
private bool HasFileContentDisposition(string contentDisposition)
{
// TODO: Strongly typed headers will take care of this for us
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return !string.IsNullOrEmpty(contentDisposition) && contentDisposition.Contains("form-data") && contentDisposition.Contains("filename=");
}
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq
private static string GetBoundary(string contentType)
{
// TODO: Strongly typed headers will take care of this for us
// TODO: Limit the length of boundary we accept. The spec says ~70 chars.
var elements = contentType.Split(' ');
var element = elements.Where(entry => entry.StartsWith("boundary=")).First();
var boundary = element.Substring("boundary=".Length);
// Remove quotes
if (boundary.Length >= 2 && boundary[0] == '"' && boundary[boundary.Length - 1] == '"')
{
boundary = boundary.Substring(1, boundary.Length - 2);
}
return boundary;
}
}
}

View File

@ -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);
}
}
}

View File

@ -4,11 +4,33 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.PipelineCore
{
public interface IFormFeature
{
Task<IReadableStringCollection> GetFormAsync(CancellationToken cancellationToken);
/// <summary>
/// Indicates if the request has a supported form content-type.
/// </summary>
bool HasFormContentType { get; }
/// <summary>
/// The parsed form, if any.
/// </summary>
IFormCollection Form { get; set; }
/// <summary>
/// Parses the request body as a form.
/// </summary>
/// <returns></returns>
IFormCollection ReadForm();
/// <summary>
/// Parses the request body as a form.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken);
}
}

View File

@ -445,12 +445,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
#endregion
public bool StartsWith(string text, StringComparison comparisonType)
public bool StartsWith([NotNull] string text, StringComparison comparisonType)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
int textLength = text.Length;
if (!HasValue || _count < textLength)
{
@ -460,12 +456,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0;
}
public bool EndsWith(string text, StringComparison comparisonType)
public bool EndsWith([NotNull] string text, StringComparison comparisonType)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
int textLength = text.Length;
if (!HasValue || _count < textLength)
{
@ -475,12 +467,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
return string.Compare(_buffer, _offset + _count - textLength, text, 0, textLength, comparisonType) == 0;
}
public bool Equals(string text, StringComparison comparisonType)
public bool Equals([NotNull] string text, StringComparison comparisonType)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
int textLength = text.Length;
if (!HasValue || _count != textLength)
{
@ -615,25 +603,17 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
}
}
public static string[] GetHeaderUnmodified(IDictionary<string, string[]> headers, string key)
public static string[] GetHeaderUnmodified([NotNull] IDictionary<string, string[]> headers, string key)
{
if (headers == null)
{
throw new ArgumentNullException("headers");
}
string[] values;
return headers.TryGetValue(key, out values) ? values : null;
}
public static void SetHeader(IDictionary<string, string[]> headers, string key, string value)
public static void SetHeader([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, string value)
{
if (headers == null)
{
throw new ArgumentNullException("headers");
}
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentNullException("key");
throw new ArgumentNullException(nameof(key));
}
if (string.IsNullOrWhiteSpace(value))
{
@ -645,15 +625,11 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
}
}
public static void SetHeaderJoined(IDictionary<string, string[]> headers, string key, params string[] values)
public static void SetHeaderJoined([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, params string[] values)
{
if (headers == null)
{
throw new ArgumentNullException("headers");
}
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentNullException("key");
throw new ArgumentNullException(nameof(key));
}
if (values == null || values.Length == 0)
{
@ -697,15 +673,11 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
return value;
}
public static void SetHeaderUnmodified(IDictionary<string, string[]> headers, string key, params string[] values)
public static void SetHeaderUnmodified([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, params string[] values)
{
if (headers == null)
{
throw new ArgumentNullException("headers");
}
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentNullException("key");
throw new ArgumentNullException(nameof(key));
}
if (values == null || values.Length == 0)
{
@ -717,16 +689,12 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
}
}
public static void SetHeaderUnmodified(IDictionary<string, string[]> headers, string key, IEnumerable<string> values)
public static void SetHeaderUnmodified([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, [NotNull] IEnumerable<string> values)
{
if (headers == null)
{
throw new ArgumentNullException("headers");
}
headers[key] = values.ToArray();
}
public static void AppendHeader(IDictionary<string, string[]> headers, string key, string values)
public static void AppendHeader([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, string values)
{
if (string.IsNullOrWhiteSpace(values))
{
@ -744,7 +712,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
}
}
public static void AppendHeaderJoined(IDictionary<string, string[]> headers, string key, params string[] values)
public static void AppendHeaderJoined([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, params string[] values)
{
if (values == null || values.Length == 0)
{
@ -762,7 +730,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
}
}
public static void AppendHeaderUnmodified(IDictionary<string, string[]> headers, string key, params string[] values)
public static void AppendHeaderUnmodified([NotNull] IDictionary<string, string[]> headers, [NotNull] string key, params string[] values)
{
if (values == null || values.Length == 0)
{
@ -801,12 +769,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
return values == null ? null : string.Join(",", values);
}
internal static string[] GetUnmodifiedValues(IDictionary<string, string[]> store, string key)
internal static string[] GetUnmodifiedValues([NotNull] IDictionary<string, string[]> store, string key)
{
if (store == null)
{
throw new ArgumentNullException("store");
}
string[] values;
return store.TryGetValue(key, out values) ? values : null;
}
@ -826,7 +790,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
// return string.IsNullOrWhiteSpace(localPort) ? localIpAddress : (localIpAddress + ":" + localPort);
//}
public static long? GetContentLength(IHeaderDictionary headers)
public static long? GetContentLength([NotNull] IHeaderDictionary headers)
{
const NumberStyles styles = NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite;
long value;
@ -840,7 +804,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
return null;
}
public static void SetContentLength(IHeaderDictionary headers, long? value)
public static void SetContentLength([NotNull] IHeaderDictionary headers, long? value)
{
if (value.HasValue)
{

View File

@ -5,9 +5,9 @@ using System.Collections.Generic;
using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.HttpFeature;
using Microsoft.AspNet.PipelineCore.Collections;
using Microsoft.AspNet.PipelineCore.Infrastructure;
using Microsoft.AspNet.WebUtilities;
using Microsoft.AspNet.WebUtilities.Collections;
namespace Microsoft.AspNet.PipelineCore
{
@ -46,7 +46,7 @@ namespace Microsoft.AspNet.PipelineCore
if (_query == null || _queryString != queryString)
{
_queryString = queryString;
_query = QueryHelpers.ParseQuery(queryString);
_query = new ReadableStringCollection(QueryHelpers.ParseQuery(queryString));
}
return _query;
}

View File

@ -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));
}
}
}
}

View File

@ -9,7 +9,6 @@ using Microsoft.AspNet.Http.Infrastructure;
using Microsoft.AspNet.HttpFeature;
using Microsoft.AspNet.PipelineCore.Collections;
using Microsoft.AspNet.PipelineCore.Infrastructure;
using Microsoft.AspNet.WebUtilities.Collections;
namespace Microsoft.AspNet.PipelineCore
{

View File

@ -17,12 +17,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
private List<AuthenticationResult> _results;
private List<string> _accepted;
public AuthenticateContext(IEnumerable<string> authenticationTypes)
public AuthenticateContext([NotNull] IEnumerable<string> authenticationTypes)
{
if (authenticationTypes == null)
{
throw new ArgumentNullException("authenticationType");
}
AuthenticationTypes = authenticationTypes;
_results = new List<AuthenticationResult>();
_accepted = new List<string>();

View File

@ -14,12 +14,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
{
private List<string> _accepted;
public ChallengeContext(IEnumerable<string> authenticationTypes, IDictionary<string, string> properties)
public ChallengeContext([NotNull] IEnumerable<string> authenticationTypes, IDictionary<string, string> properties)
{
if (authenticationTypes == null)
{
throw new ArgumentNullException();
}
AuthenticationTypes = authenticationTypes;
Properties = properties ?? new Dictionary<string, string>(StringComparer.Ordinal);
_accepted = new List<string>();
@ -33,7 +29,7 @@ namespace Microsoft.AspNet.PipelineCore.Security
{
get { return _accepted; }
}
public void Accept(string authenticationType, IDictionary<string, object> description)
{
_accepted.Add(authenticationType);

View File

@ -12,12 +12,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
{
private List<string> _accepted;
public SignInContext(IEnumerable<ClaimsIdentity> identities, IDictionary<string, string> dictionary)
public SignInContext([NotNull] IEnumerable<ClaimsIdentity> identities, IDictionary<string, string> dictionary)
{
if (identities == null)
{
throw new ArgumentNullException("identities");
}
Identities = identities;
Properties = dictionary ?? new Dictionary<string, string>(StringComparer.Ordinal);
_accepted = new List<string>();

View File

@ -11,12 +11,8 @@ namespace Microsoft.AspNet.PipelineCore.Security
{
private List<string> _accepted;
public SignOutContext(IEnumerable<string> authenticationTypes)
public SignOutContext([NotNull] IEnumerable<string> authenticationTypes)
{
if (authenticationTypes == null)
{
throw new ArgumentNullException("authenticationTypes");
}
AuthenticationTypes = authenticationTypes;
_accepted = new List<string>();
}

View File

@ -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.");
}
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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));
}
}
}

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.WebUtilities
{
@ -50,9 +49,49 @@ namespace Microsoft.AspNet.WebUtilities
/// </summary>
/// <param name="text">The raw query string value, with or without the leading '?'.</param>
/// <returns>A collection of parsed keys and values.</returns>
public static IReadableStringCollection ParseQuery(string text)
public static IDictionary<string, string[]> ParseQuery(string queryString)
{
return ParsingHelpers.GetQuery(text);
if (!string.IsNullOrEmpty(queryString) && queryString[0] == '?')
{
queryString = queryString.Substring(1);
}
var accumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
int textLength = queryString.Length;
int equalIndex = queryString.IndexOf('=');
if (equalIndex == -1)
{
equalIndex = textLength;
}
int scanIndex = 0;
while (scanIndex < textLength)
{
int delimiterIndex = queryString.IndexOf('&', scanIndex);
if (delimiterIndex == -1)
{
delimiterIndex = textLength;
}
if (equalIndex < delimiterIndex)
{
while (scanIndex != equalIndex && char.IsWhiteSpace(queryString[scanIndex]))
{
++scanIndex;
}
string name = queryString.Substring(scanIndex, equalIndex - scanIndex);
string value = queryString.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1);
accumulator.Append(
Uri.UnescapeDataString(name.Replace('+', ' ')),
Uri.UnescapeDataString(value.Replace('+', ' ')));
equalIndex = queryString.IndexOf('=', delimiterIndex);
if (equalIndex == -1)
{
equalIndex = textLength;
}
}
scanIndex = delimiterIndex + 1;
}
return accumulator.GetResults();
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -9,6 +9,7 @@
"aspnetcore50": {
"dependencies": {
"System.Diagnostics.Debug": "4.0.10-beta-*",
"System.IO.FileSystem": "4.0.0-beta-*",
"System.Runtime": "4.0.20-beta-*"
}
}

View File

@ -1,72 +1,266 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.HttpFeature;
using Moq;
using Microsoft.AspNet.WebUtilities;
using Xunit;
namespace Microsoft.AspNet.PipelineCore.Tests
namespace Microsoft.AspNet.PipelineCore
{
public class FormFeatureTests
{
[Fact]
public async Task GetFormAsync_ReturnsParsedFormCollection()
public async Task ReadFormAsync_SimpleData_ReturnsParsedFormCollection()
{
// Arrange
var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2");
var features = new Mock<IFeatureCollection>();
var request = new Mock<IHttpRequestFeature>();
request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent));
var context = new DefaultHttpContext();
context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
context.Request.Body = new MemoryStream(formContent);
object value = request.Object;
features.Setup(f => f.TryGetValue(typeof(IHttpRequestFeature), out value))
.Returns(true);
var provider = new FormFeature(features.Object);
// Not cached yet
var formFeature = context.GetFeature<IFormFeature>();
Assert.Null(formFeature);
// Act
var formCollection = await provider.GetFormAsync(CancellationToken.None);
var formCollection = await context.Request.ReadFormAsync();
// Assert
Assert.Equal("bar", formCollection["foo"]);
Assert.Equal("2", formCollection["baz"]);
// Cached
formFeature = context.GetFeature<IFormFeature>();
Assert.NotNull(formFeature);
Assert.NotNull(formFeature.Form);
Assert.Same(formFeature.Form, formCollection);
}
[Fact]
public async Task GetFormAsync_CachesFormCollectionPerBodyStream()
public async Task ReadFormAsync_EmptyKeyAtEndAllowed()
{
// Arrange
var formContent1 = Encoding.UTF8.GetBytes("foo=bar&baz=2");
var formContent2 = Encoding.UTF8.GetBytes("collection2=value");
var features = new Mock<IFeatureCollection>();
var request = new Mock<IHttpRequestFeature>();
request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent1));
var formContent = Encoding.UTF8.GetBytes("=bar");
var body = new MemoryStream(formContent);
object value = request.Object;
features.Setup(f => f.TryGetValue(typeof(IHttpRequestFeature), out value))
.Returns(true);
var formCollection = await FormReader.ReadFormAsync(body);
var provider = new FormFeature(features.Object);
// Assert
Assert.Equal("bar", formCollection[""].FirstOrDefault());
}
// Act - 1
var formCollection = await provider.GetFormAsync(CancellationToken.None);
[Fact]
public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed()
{
// Arrange
var formContent = Encoding.UTF8.GetBytes("=bar&baz=2");
var body = new MemoryStream(formContent);
// Assert - 1
Assert.Equal("bar", formCollection["foo"]);
Assert.Equal("2", formCollection["baz"]);
Assert.Same(formCollection, await provider.GetFormAsync(CancellationToken.None));
var formCollection = await FormReader.ReadFormAsync(body);
// Act - 2
request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent2));
formCollection = await provider.GetFormAsync(CancellationToken.None);
// Assert
Assert.Equal("bar", formCollection[""].FirstOrDefault());
Assert.Equal("2", formCollection["baz"].FirstOrDefault());
}
// Assert - 2
Assert.Equal("value", formCollection["collection2"]);
[Fact]
public async Task ReadFormAsync_EmptyValuedAtEndAllowed()
{
// Arrange
var formContent = Encoding.UTF8.GetBytes("foo=");
var body = new MemoryStream(formContent);
var formCollection = await FormReader.ReadFormAsync(body);
// Assert
Assert.Equal("", formCollection["foo"].FirstOrDefault());
}
[Fact]
public async Task ReadFormAsync_EmptyValuedWithAdditionalEntryAllowed()
{
// Arrange
var formContent = Encoding.UTF8.GetBytes("foo=&baz=2");
var body = new MemoryStream(formContent);
var formCollection = await FormReader.ReadFormAsync(body);
// Assert
Assert.Equal("", formCollection["foo"].FirstOrDefault());
Assert.Equal("2", formCollection["baz"].FirstOrDefault());
}
private const string MultipartContentType = "multipart/form-data; boundary=WebKitFormBoundary5pDRpGheQXaM8k3T";
private const string EmptyMultipartForm =
@"--WebKitFormBoundary5pDRpGheQXaM8k3T--";
private const string MultipartFormWithField =
@"--WebKitFormBoundary5pDRpGheQXaM8k3T
Content-Disposition: form-data; name=""description""
Foo
--WebKitFormBoundary5pDRpGheQXaM8k3T--";
private const string MultipartFormWithFile =
@"--WebKitFormBoundary5pDRpGheQXaM8k3T
Content-Disposition: form-data; name=""myfile1""; filename=""temp.html""
Content-Type: text/html
<html><body>Hello World</body></html>
--WebKitFormBoundary5pDRpGheQXaM8k3T--";
private const string MultipartFormWithFieldAndFile =
@"--WebKitFormBoundary5pDRpGheQXaM8k3T
Content-Disposition: form-data; name=""description""
Foo
--WebKitFormBoundary5pDRpGheQXaM8k3T
Content-Disposition: form-data; name=""myfile1""; filename=""temp.html""
Content-Type: text/html
<html><body>Hello World</body></html>
--WebKitFormBoundary5pDRpGheQXaM8k3T--";
[Fact]
public async Task ReadForm_EmptyMultipart_ReturnsParsedFormCollection()
{
var formContent = Encoding.UTF8.GetBytes(EmptyMultipartForm);
var context = new DefaultHttpContext();
context.Request.ContentType = MultipartContentType;
context.Request.Body = new MemoryStream(formContent);
// Not cached yet
var formFeature = context.GetFeature<IFormFeature>();
Assert.Null(formFeature);
var formCollection = context.Request.Form;
Assert.NotNull(formCollection);
// Cached
formFeature = context.GetFeature<IFormFeature>();
Assert.NotNull(formFeature);
Assert.NotNull(formFeature.Form);
Assert.Same(formCollection, formFeature.Form);
Assert.Same(formCollection, await context.Request.ReadFormAsync());
// Content
Assert.Equal(0, formCollection.Count);
Assert.NotNull(formCollection.Files);
Assert.Equal(0, formCollection.Files.Count);
}
[Fact]
public async Task ReadForm_MultipartWithField_ReturnsParsedFormCollection()
{
var formContent = Encoding.UTF8.GetBytes(MultipartFormWithField);
var context = new DefaultHttpContext();
context.Request.ContentType = MultipartContentType;
context.Request.Body = new MemoryStream(formContent);
// Not cached yet
var formFeature = context.GetFeature<IFormFeature>();
Assert.Null(formFeature);
var formCollection = context.Request.Form;
Assert.NotNull(formCollection);
// Cached
formFeature = context.GetFeature<IFormFeature>();
Assert.NotNull(formFeature);
Assert.NotNull(formFeature.Form);
Assert.Same(formCollection, formFeature.Form);
Assert.Same(formCollection, await context.Request.ReadFormAsync());
// Content
Assert.Equal(1, formCollection.Count);
Assert.Equal("Foo", formCollection["description"]);
Assert.NotNull(formCollection.Files);
Assert.Equal(0, formCollection.Files.Count);
}
[Fact]
public async Task ReadFormAsync_MultipartWithFile_ReturnsParsedFormCollection()
{
var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFile);
var context = new DefaultHttpContext();
context.Request.ContentType = MultipartContentType;
context.Request.Body = new MemoryStream(formContent);
// Not cached yet
var formFeature = context.GetFeature<IFormFeature>();
Assert.Null(formFeature);
var formCollection = await context.Request.ReadFormAsync();
Assert.NotNull(formCollection);
// Cached
formFeature = context.GetFeature<IFormFeature>();
Assert.NotNull(formFeature);
Assert.NotNull(formFeature.Form);
Assert.Same(formFeature.Form, formCollection);
Assert.Same(formCollection, context.Request.Form);
// Content
Assert.Equal(0, formCollection.Count);
Assert.NotNull(formCollection.Files);
Assert.Equal(1, formCollection.Files.Count);
var file = formCollection.Files["myfile1"];
Assert.Equal("text/html", file.ContentType);
Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition);
var body = file.OpenReadStream();
using (var reader = new StreamReader(body))
{
var content = reader.ReadToEnd();
Assert.Equal(content, "<html><body>Hello World</body></html>");
}
}
[Fact]
public async Task ReadFormAsync_MultipartWithFieldAndFile_ReturnsParsedFormCollection()
{
var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFieldAndFile);
var context = new DefaultHttpContext();
context.Request.ContentType = MultipartContentType;
context.Request.Body = new MemoryStream(formContent);
// Not cached yet
var formFeature = context.GetFeature<IFormFeature>();
Assert.Null(formFeature);
var formCollection = await context.Request.ReadFormAsync();
Assert.NotNull(formCollection);
// Cached
formFeature = context.GetFeature<IFormFeature>();
Assert.NotNull(formFeature);
Assert.NotNull(formFeature.Form);
Assert.Same(formFeature.Form, formCollection);
Assert.Same(formCollection, context.Request.Form);
// Content
Assert.Equal(1, formCollection.Count);
Assert.Equal("Foo", formCollection["description"]);
Assert.NotNull(formCollection.Files);
Assert.Equal(1, formCollection.Files.Count);
var file = formCollection.Files["myfile1"];
Assert.Equal("text/html", file.ContentType);
Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition);
var body = file.OpenReadStream();
using (var reader = new StreamReader(body))
{
var content = reader.ReadToEnd();
Assert.Equal(content, "<html><body>Hello World</body></html>");
}
}
}
}

View File

@ -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());
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Xunit;
namespace Microsoft.AspNet.WebUtilities
@ -13,8 +14,8 @@ namespace Microsoft.AspNet.WebUtilities
{
var collection = QueryHelpers.ParseQuery("?key1=value1&key2=value2");
Assert.Equal(2, collection.Count);
Assert.Equal("value1", collection["key1"]);
Assert.Equal("value2", collection["key2"]);
Assert.Equal("value1", collection["key1"].FirstOrDefault());
Assert.Equal("value2", collection["key2"].FirstOrDefault());
}
[Fact]
@ -22,8 +23,8 @@ namespace Microsoft.AspNet.WebUtilities
{
var collection = QueryHelpers.ParseQuery("key1=value1&key2=value2");
Assert.Equal(2, collection.Count);
Assert.Equal("value1", collection["key1"]);
Assert.Equal("value2", collection["key2"]);
Assert.Equal("value1", collection["key1"].FirstOrDefault());
Assert.Equal("value2", collection["key2"].FirstOrDefault());
}
[Fact]
@ -31,8 +32,8 @@ namespace Microsoft.AspNet.WebUtilities
{
var collection = QueryHelpers.ParseQuery("?key1=valueA&key2=valueB&key1=valueC");
Assert.Equal(2, collection.Count);
Assert.Equal("valueA,valueC", collection["key1"]);
Assert.Equal("valueB", collection["key2"]);
Assert.Equal(new[] { "valueA", "valueC" }, collection["key1"]);
Assert.Equal("valueB", collection["key2"].FirstOrDefault());
}
[Fact]
@ -40,8 +41,8 @@ namespace Microsoft.AspNet.WebUtilities
{
var collection = QueryHelpers.ParseQuery("?key1=&key2=");
Assert.Equal(2, collection.Count);
Assert.Equal(string.Empty, collection["key1"]);
Assert.Equal(string.Empty, collection["key2"]);
Assert.Equal(string.Empty, collection["key1"].FirstOrDefault());
Assert.Equal(string.Empty, collection["key2"].FirstOrDefault());
}
[Fact]
@ -49,7 +50,7 @@ namespace Microsoft.AspNet.WebUtilities
{
var collection = QueryHelpers.ParseQuery("?=value1&=");
Assert.Equal(1, collection.Count);
Assert.Equal("value1,", collection[""]);
Assert.Equal(new[] { "value1", "" }, collection[""]);
}
}
}