diff --git a/HttpAbstractions.sln b/HttpAbstractions.sln index a2737cba10..d0bb133101 100644 --- a/HttpAbstractions.sln +++ b/HttpAbstractions.sln @@ -21,11 +21,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.PipelineCo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.PipelineCore.k10", "src\Microsoft.AspNet.PipelineCore\Microsoft.AspNet.PipelineCore.k10.csproj", "{E31CF247-FDA9-4007-B194-A7DBAC18532C}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D38DDB2B-1138-4F45-8A6A-9499E880F620}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1D737C82-F2F1-40B6-AE95-A3D878612E91}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.PipelineCore.Tests", "test\Microsoft.AspNet.PipelineCore.Tests\Microsoft.AspNet.PipelineCore.Tests.csproj", "{86942914-0334-4352-87ED-B971281C74E2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.FeatureModel.Tests", "test\Microsoft.AspNet.FeatureModel.Tests\Microsoft.AspNet.FeatureModel.Tests.csproj", "{8C671AE3-1188-499F-A2A2-3A0B117B33CE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Abstractions.Owin.net45", "src\Microsoft.AspNet.Abstractions.Owin\Microsoft.AspNet.Abstractions.Owin.net45.csproj", "{E6B7056F-547E-4B96-9E58-858DDDAE75D3}" EndProject diff --git a/src/Microsoft.AspNet.Abstractions/CookieOptions.cs b/src/Microsoft.AspNet.Abstractions/CookieOptions.cs new file mode 100644 index 0000000000..c8505cae1b --- /dev/null +++ b/src/Microsoft.AspNet.Abstractions/CookieOptions.cs @@ -0,0 +1,48 @@ +using System; + +namespace Microsoft.AspNet.Abstractions +{ + /// + /// Options used to create a new cookie. + /// + public class CookieOptions + { + /// + /// Creates a default cookie with a path of '/'. + /// + public CookieOptions() + { + Path = "/"; + } + + /// + /// Gets or sets the domain to associate the cookie with. + /// + /// The domain to associate the cookie with. + public string Domain { get; set; } + + /// + /// Gets or sets the cookie path. + /// + /// The cookie path. + public string Path { get; set; } + + /// + /// Gets or sets the expiration date and time for the cookie. + /// + /// The expiration date and time for the cookie. + public DateTime? Expires { get; set; } + + /// + /// Gets or sets a value that indicates whether to transmit the cookie using Secure Sockets Layer (SSL)—that is, over HTTPS only. + /// + /// true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false. + public bool Secure { get; set; } + + /// + /// Gets or sets a value that indicates whether a cookie is accessible by client-side script. + /// + /// true if a cookie is accessible by client-side script; otherwise, false. + public bool HttpOnly { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Abstractions/HostString.cs b/src/Microsoft.AspNet.Abstractions/HostString.cs new file mode 100644 index 0000000000..0847c7421e --- /dev/null +++ b/src/Microsoft.AspNet.Abstractions/HostString.cs @@ -0,0 +1,197 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Microsoft.AspNet.Abstractions +{ + /// + /// Represents the host portion of a Uri can be used to construct Uri's properly formatted and encoded for use in + /// HTTP headers. + /// + public struct HostString : IEquatable + { + private readonly string _value; + + /// + /// Creates a new HostString without modification. The value should be Unicode rather than punycode, and may have a port. + /// IPv4 and IPv6 addresses are also allowed, and also may have ports. + /// + /// + public HostString(string value) + { + _value = value; + } + + /// + /// Returns the original value from the constructor. + /// + public string Value + { + get { return _value; } + } + + /// + /// Returns the value as normalized by ToUriComponent(). + /// + /// + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Returns the value properly formatted and encoded for use in a URI in a HTTP header. + /// Any Unicode is converted to punycode. IPv6 addresses will have brackets added if they are missing. + /// + /// + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "Only the host segment of a uri is returned.")] + public string ToUriComponent() + { + int index; + if (string.IsNullOrEmpty(_value)) + { + return string.Empty; + } + else if (_value.IndexOf('[') >= 0) + { + // IPv6 in brackets [::1], maybe with port + return _value; + } + else if ((index = _value.IndexOf(':')) >= 0 + && index < _value.Length - 1 + && _value.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + return "[" + _value + "]"; + } + else if (index >= 0) + { + // Has a port + string port = _value.Substring(index); + IdnMapping mapping = new IdnMapping(); + return mapping.GetAscii(_value, 0, index) + port; + } + else + { + IdnMapping mapping = new IdnMapping(); + return mapping.GetAscii(_value); + } + } + + /// + /// Creates a new HostString from the given uri component. + /// Any punycode will be converted to Unicode. + /// + /// + /// + [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification = "Only the host segment of a uri is provided.")] + public static HostString FromUriComponent(string uriComponent) + { + if (!string.IsNullOrEmpty(uriComponent)) + { + int index; + if (uriComponent.IndexOf('[') >= 0) + { + // IPv6 in brackets [::1], maybe with port + } + else if ((index = uriComponent.IndexOf(':')) >= 0 + && index < uriComponent.Length - 1 + && uriComponent.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + } + else if (uriComponent.IndexOf("xn--", StringComparison.Ordinal) >= 0) + { + // Contains punycode + if (index >= 0) + { + // Has a port + string port = uriComponent.Substring(index); + IdnMapping mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent, 0, index) + port; + } + else + { + IdnMapping mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent); + } + } + } + return new HostString(uriComponent); + } + + /// + /// Creates a new HostString from the host and port of the give Uri instance. + /// Punycode will be converted to Unicode. + /// + /// + /// + public static HostString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException("uri"); + } + return new HostString(uri.GetComponents( +#if !NET40 + UriComponents.NormalizedHost | // Always convert punycode to Unicode. +#endif + UriComponents.HostAndPort, UriFormat.Unescaped)); + } + + /// + /// Compares the equality of the Value property, ignoring case. + /// + /// + /// + public bool Equals(HostString other) + { + return string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Compares against the given object only if it is a HostString. + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + return obj is HostString && Equals((HostString)obj); + } + + /// + /// Gets a hash code for the value. + /// + /// + public override int GetHashCode() + { + return (_value != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); + } + + /// + /// Compares the two instances for equality. + /// + /// + /// + /// + public static bool operator ==(HostString left, HostString right) + { + return left.Equals(right); + } + + /// + /// Compares the two instances for inequality. + /// + /// + /// + /// + public static bool operator !=(HostString left, HostString right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Microsoft.AspNet.Abstractions/HttpRequest.cs b/src/Microsoft.AspNet.Abstractions/HttpRequest.cs index 7277195cc7..40a7860af5 100644 --- a/src/Microsoft.AspNet.Abstractions/HttpRequest.cs +++ b/src/Microsoft.AspNet.Abstractions/HttpRequest.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading; namespace Microsoft.AspNet.Abstractions { @@ -9,10 +10,113 @@ namespace Microsoft.AspNet.Abstractions public abstract HttpContext HttpContext { get; } - public abstract Uri Uri { get; } + /// + /// Gets or set the HTTP method. + /// + /// The HTTP method. + public abstract string Method { get; set; } + + /// + /// Gets or set the HTTP request scheme from owin.RequestScheme. + /// + /// The HTTP request scheme from owin.RequestScheme. + public abstract string Scheme { get; set; } + + /// + /// Returns true if the owin.RequestScheme is https. + /// + /// true if this request is using https; otherwise, false. + public abstract bool IsSecure { get; } + + /// + /// Gets or set the Host header. May include the port. + /// + /// The Host header. + public abstract HostString Host { get; set; } + + /// + /// Gets or set the owin.RequestPathBase. + /// + /// The owin.RequestPathBase. public abstract PathString PathBase { get; set; } + + /// + /// Gets or set the request path from owin.RequestPath. + /// + /// The request path from owin.RequestPath. public abstract PathString Path { get; set; } + + /// + /// Gets or set the query string from owin.RequestQueryString. + /// + /// The query string from owin.RequestQueryString. public abstract QueryString QueryString { get; set; } + + /// + /// Gets the query value collection parsed from owin.RequestQueryString. + /// + /// The query value collection parsed from owin.RequestQueryString. + public abstract IReadableStringCollection Query { get; } + + /// + /// Gets the uniform resource identifier (URI) associated with the request. + /// + /// The uniform resource identifier (URI) associated with the request. + public abstract Uri Uri { get; } + + /// + /// Gets or set the owin.RequestProtocol. + /// + /// The owin.RequestProtocol. + public abstract string Protocol { get; set; } + + /// + /// Gets the request headers. + /// + /// The request headers. + public abstract IHeaderDictionary Headers { get; } + + /// + /// Gets the collection of Cookies for this request. + /// + /// The collection of Cookies for this request. + public abstract IReadableStringCollection Cookies { get; } + + /// + /// Gets or sets the Content-Type header. + /// + /// The Content-Type header. + // (TODO header conventions?) public abstract string ContentType { get; set; } + + /// + /// Gets or sets the Cache-Control header. + /// + /// The Cache-Control header. + // (TODO header conventions?) public abstract string CacheControl { get; set; } + + /// + /// Gets or sets the Media-Type header. + /// + /// The Media-Type header. + // (TODO header conventions?) public abstract string MediaType { get; set; } + + /// + /// Gets or set the Accept header. + /// + /// The Accept header. + // (TODO header conventions?) public abstract string Accept { get; set; } + + /// + /// Gets or set the owin.RequestBody Stream. + /// + /// The owin.RequestBody Stream. public abstract Stream Body { get; set; } + + /// + /// Gets or sets the cancellation token for the request. + /// + /// The cancellation token for the request. + public abstract CancellationToken CallCanceled { get; set; } + } } diff --git a/src/Microsoft.AspNet.Abstractions/IFormCollection.cs b/src/Microsoft.AspNet.Abstractions/IFormCollection.cs new file mode 100644 index 0000000000..56862e6be6 --- /dev/null +++ b/src/Microsoft.AspNet.Abstractions/IFormCollection.cs @@ -0,0 +1,9 @@ +namespace Microsoft.AspNet.Abstractions +{ + /// + /// Contains the parsed form values. + /// + public interface IFormCollection : IReadableStringCollection + { + } +} diff --git a/src/Microsoft.AspNet.Abstractions/IHeaderDictionary.cs b/src/Microsoft.AspNet.Abstractions/IHeaderDictionary.cs new file mode 100644 index 0000000000..7039b8b87e --- /dev/null +++ b/src/Microsoft.AspNet.Abstractions/IHeaderDictionary.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.Abstractions +{ + /// + /// Represents request and response headers + /// + public interface IHeaderDictionary : IReadableStringCollection, IDictionary + { + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a single string or null if the key is not present. + new string this[string key] { get; set; } + + /// + /// Get the associated values from the collection separated into individual values. + /// Quoted values will not be split, and the quotes will be removed. + /// + /// The header name. + /// the associated values from the collection separated into individual values, or null if the key is not present. + IList GetCommaSeparatedValues(string key); + + /// + /// Add a new value. Appends to the header if already present + /// + /// The header name. + /// The header value. + void Append(string key, string value); + + /// + /// Add new values. Each item remains a separate array entry. + /// + /// The header name. + /// The header values. + void AppendValues(string key, params string[] values); + + /// + /// Quotes any values containing comas, and then coma joins all of the values with any existing values. + /// + /// The header name. + /// The header values. + void AppendCommaSeparatedValues(string key, params string[] values); + + /// + /// Sets a specific header value. + /// + /// The header name. + /// The header value. + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Set", Justification = "Re-evaluate later.")] + void Set(string key, string value); + + /// + /// Sets the specified header values without modification. + /// + /// The header name. + /// The header values. + void SetValues(string key, params string[] values); + + /// + /// Quotes any values containing comas, and then coma joins all of the values. + /// + /// The header name. + /// The header values. + void SetCommaSeparatedValues(string key, params string[] values); + } +} diff --git a/src/Microsoft.AspNet.Abstractions/IReadableStringCollection.cs b/src/Microsoft.AspNet.Abstractions/IReadableStringCollection.cs new file mode 100644 index 0000000000..dc3f72c769 --- /dev/null +++ b/src/Microsoft.AspNet.Abstractions/IReadableStringCollection.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.Abstractions +{ + /// + /// Accessors for headers, query, forms, etc. + /// + public interface IReadableStringCollection : IEnumerable> + { + /// + /// Get the associated value from the collection. Multiple values will be merged. + /// Returns null if the key is not present. + /// + /// + /// + string this[string key] { get; } + + // Joined + + /// + /// Get the associated value from the collection. Multiple values will be merged. + /// Returns null if the key is not present. + /// + /// + /// + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get", Justification = "Re-evaluate later.")] + string Get(string key); + + // Joined + + /// + /// Get the associated values from the collection in their original format. + /// Returns null if the key is not present. + /// + /// + /// + IList GetValues(string key); + + // Raw + } +} diff --git a/src/Microsoft.AspNet.Abstractions/PathString.cs b/src/Microsoft.AspNet.Abstractions/PathString.cs index 8cfab5912d..c497d40bf3 100644 --- a/src/Microsoft.AspNet.Abstractions/PathString.cs +++ b/src/Microsoft.AspNet.Abstractions/PathString.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; +using System; using System.Linq; namespace Microsoft.AspNet.Abstractions diff --git a/src/Microsoft.AspNet.Abstractions/QueryString.cs b/src/Microsoft.AspNet.Abstractions/QueryString.cs index e80f5eedcf..9d96419788 100644 --- a/src/Microsoft.AspNet.Abstractions/QueryString.cs +++ b/src/Microsoft.AspNet.Abstractions/QueryString.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; +using System; namespace Microsoft.AspNet.Abstractions { diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs new file mode 100644 index 0000000000..7054d36350 --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNet.Abstractions; +using System.Collections.Generic; + +namespace Microsoft.AspNet.PipelineCore.Collections +{ + /// + /// Contains the parsed form values. + /// + public class FormCollection : ReadableStringCollection, IFormCollection + { + /// + /// Initializes a new instance of the class. + /// + /// The store for the form. + public FormCollection(IDictionary store) + : base(store) + { + } + } +} diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs b/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs new file mode 100644 index 0000000000..8847fa66fc --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Abstractions.Infrastructure; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.PipelineCore.Infrastructure; + +namespace Microsoft.AspNet.PipelineCore.Collections +{ + /// + /// Represents a wrapper for owin.RequestHeaders and owin.ResponseHeaders. + /// + public class HeaderDictionary : IHeaderDictionary + { + /// + /// Initializes a new instance of the class. + /// + /// The underlying data store. + public HeaderDictionary(IDictionary store) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + + Store = store; + } + + private IDictionary Store { get; set; } + + /// + /// Gets an that contains the keys in the ;. + /// + /// An that contains the keys in the . + public ICollection Keys + { + get { return Store.Keys; } + } + + /// + /// + /// + public ICollection Values + { + get { return Store.Values; } + } + + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count + { + get { return Store.Count; } + } + + /// + /// Gets a value that indicates whether the is in read-only mode. + /// + /// true if the is in read-only mode; otherwise, false. + public bool IsReadOnly + { + get { return Store.IsReadOnly; } + } + + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a single string or null if the key is not present. + public string this[string key] + { + get { return Get(key); } + set { Set(key, value); } + } + + /// + /// Throws KeyNotFoundException if the key is not present. + /// + /// The header name. + /// + string[] IDictionary.this[string key] + { + get { return Store[key]; } + set { Store[key] = value; } + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public IEnumerator> GetEnumerator() + { + return Store.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Get the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a single string or null if the key is not present. + public string Get(string key) + { + return ParsingHelpers.GetHeader(Store, key); + } + + /// + /// Get the associated values from the collection without modification. + /// + /// The header name. + /// the associated value from the collection without modification, or null if the key is not present. + public IList GetValues(string key) + { + return ParsingHelpers.GetHeaderUnmodified(Store, key); + } + + /// + /// Get the associated values from the collection separated into individual values. + /// Quoted values will not be split, and the quotes will be removed. + /// + /// The header name. + /// the associated values from the collection separated into individual values, or null if the key is not present. + public IList GetCommaSeparatedValues(string key) + { + IEnumerable values = ParsingHelpers.GetHeaderSplit(Store, key); + return values == null ? null : values.ToList(); + } + + /// + /// Add a new value. Appends to the header if already present + /// + /// The header name. + /// The header value. + public void Append(string key, string value) + { + ParsingHelpers.AppendHeader(Store, key, value); + } + + /// + /// Add new values. Each item remains a separate array entry. + /// + /// The header name. + /// The header values. + public void AppendValues(string key, params string[] values) + { + ParsingHelpers.AppendHeaderUnmodified(Store, key, values); + } + + /// + /// Quotes any values containing comas, and then coma joins all of the values with any existing values. + /// + /// The header name. + /// The header values. + public void AppendCommaSeparatedValues(string key, params string[] values) + { + ParsingHelpers.AppendHeaderJoined(Store, key, values); + } + + /// + /// Sets a specific header value. + /// + /// The header name. + /// The header value. + public void Set(string key, string value) + { + ParsingHelpers.SetHeader(Store, key, value); + } + + /// + /// Sets the specified header values without modification. + /// + /// The header name. + /// The header values. + public void SetValues(string key, params string[] values) + { + ParsingHelpers.SetHeaderUnmodified(Store, key, values); + } + + /// + /// Quotes any values containing comas, and then coma joins all of the values. + /// + /// The header name. + /// The header values. + public void SetCommaSeparatedValues(string key, params string[] values) + { + ParsingHelpers.SetHeaderJoined(Store, key, values); + } + + /// + /// Adds the given header and values to the collection. + /// + /// The header name. + /// The header values. + public void Add(string key, string[] value) + { + Store.Add(key, value); + } + + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + return Store.ContainsKey(key); + } + + /// + /// Removes the given header from the collection. + /// + /// The header name. + /// true if the specified object was removed from the collection; otherwise, false. + public bool Remove(string key) + { + return Store.Remove(key); + } + + /// + /// Retrieves a value from the dictionary. + /// + /// The header name. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out string[] value) + { + return Store.TryGetValue(key, out value); + } + + /// + /// Adds a new list of items to the collection. + /// + /// The item to add. + public void Add(KeyValuePair item) + { + Store.Add(item); + } + + /// + /// Clears the entire list of objects. + /// + public void Clear() + { + Store.Clear(); + } + + /// + /// Returns a value indicating whether the specified object occurs within this collection. + /// + /// The item. + /// true if the specified object occurs within this collection; otherwise, false. + public bool Contains(KeyValuePair item) + { + return Store.Contains(item); + } + + /// + /// Copies the elements to a one-dimensional Array instance at the specified index. + /// + /// The one-dimensional Array that is the destination of the specified objects copied from the . + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + Store.CopyTo(array, arrayIndex); + } + + /// + /// Removes the given item from the the collection. + /// + /// The item. + /// true if the specified object was removed from the collection; otherwise, false. + public bool Remove(KeyValuePair item) + { + return Store.Remove(item); + } + } +} diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs new file mode 100644 index 0000000000..f06a8aa5a0 --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.AspNet.Abstractions.Infrastructure; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.PipelineCore.Infrastructure; + +namespace Microsoft.AspNet.PipelineCore.Collections +{ + /// + /// Accessors for query, forms, etc. + /// + public class ReadableStringCollection : IReadableStringCollection + { + /// + /// Create a new wrapper + /// + /// + public ReadableStringCollection(IDictionary store) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + + Store = store; + } + + private IDictionary Store { get; set; } + + /// + /// Get the associated value from the collection. Multiple values will be merged. + /// Returns null if the key is not present. + /// + /// + /// + public string this[string key] + { + get { return Get(key); } + } + + /// + /// Get the associated value from the collection. Multiple values will be merged. + /// Returns null if the key is not present. + /// + /// + /// + public string Get(string key) + { + return ParsingHelpers.GetJoinedValue(Store, key); + } + + /// + /// Get the associated values from the collection in their original format. + /// Returns null if the key is not present. + /// + /// + /// + public IList GetValues(string key) + { + string[] values; + Store.TryGetValue(key, out values); + return values; + } + + /// + /// + /// + /// + public IEnumerator> GetEnumerator() + { + return Store.GetEnumerator(); + } + + /// + /// + /// + /// + IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/RequestCookieCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/RequestCookieCollection.cs new file mode 100644 index 0000000000..91b1577f28 --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/Collections/RequestCookieCollection.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.PipelineCore.Collections +{ + /// + /// A wrapper for the request Cookie header + /// + public class RequestCookieCollection : IEnumerable> + { + /// + /// Create a new wrapper + /// + /// + public RequestCookieCollection(IDictionary store) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + + Store = store; + } + + private IDictionary Store { get; set; } + + /// + /// Returns null rather than throwing KeyNotFoundException + /// + /// + /// + public string this[string key] + { + get + { + string value; + Store.TryGetValue(key, out value); + return value; + } + } + + /// + /// + /// + /// + public IEnumerator> GetEnumerator() + { + return Store.GetEnumerator(); + } + + /// + /// + /// + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookieCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookieCollection.cs new file mode 100644 index 0000000000..78703285cf --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookieCollection.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Abstractions.Infrastructure; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.PipelineCore.Collections +{ + /// + /// A wrapper for the response Set-Cookie header + /// + public class ResponseCookieCollection + { + /// + /// Create a new wrapper + /// + /// + public ResponseCookieCollection(IHeaderDictionary headers) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + + Headers = headers; + } + + private IHeaderDictionary Headers { get; set; } + + /// + /// Add a new cookie and value + /// + /// + /// + public void Append(string key, string value) + { + Headers.AppendValues(Constants.Headers.SetCookie, Uri.EscapeDataString(key) + "=" + Uri.EscapeDataString(value) + "; path=/"); + } + + /// + /// Add a new cookie + /// + /// + /// + /// + public void Append(string key, string value, 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; + + string setCookieValue = string.Concat( + Uri.EscapeDataString(key), + "=", + Uri.EscapeDataString(value ?? string.Empty), + !domainHasValue ? null : "; domain=", + !domainHasValue ? null : options.Domain, + !pathHasValue ? null : "; path=", + !pathHasValue ? null : options.Path, + !expiresHasValue ? null : "; expires=", + !expiresHasValue ? null : options.Expires.Value.ToString("ddd, dd-MMM-yyyy HH:mm:ss ", CultureInfo.InvariantCulture) + "GMT", + !options.Secure ? null : "; secure", + !options.HttpOnly ? null : "; HttpOnly"); + Headers.AppendValues("Set-Cookie", setCookieValue); + } + + /// + /// Sets an expired cookie + /// + /// + public void Delete(string key) + { + Func predicate = value => value.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase); + + var deleteCookies = new[] { Uri.EscapeDataString(key) + "=; expires=Thu, 01-Jan-1970 00:00:00 GMT" }; + IList existingValues = Headers.GetValues(Constants.Headers.SetCookie); + if (existingValues == null || existingValues.Count == 0) + { + Headers.SetValues(Constants.Headers.SetCookie, deleteCookies); + } + else + { + Headers.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !predicate(value)).Concat(deleteCookies).ToArray()); + } + } + + /// + /// Sets an expired cookie + /// + /// + /// + public void Delete(string key, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException("options"); + } + + bool domainHasValue = !string.IsNullOrEmpty(options.Domain); + bool pathHasValue = !string.IsNullOrEmpty(options.Path); + + Func rejectPredicate; + if (domainHasValue) + { + rejectPredicate = value => + value.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase) && + value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1; + } + else if (pathHasValue) + { + rejectPredicate = value => + value.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase) && + value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1; + } + else + { + rejectPredicate = value => value.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase); + } + + IList existingValues = Headers.GetValues(Constants.Headers.SetCookie); + if (existingValues != null) + { + Headers.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray()); + } + + Append(key, string.Empty, new CookieOptions + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + } + } +} diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs b/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs index 73860a9f32..4a5be161e2 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs +++ b/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs @@ -70,5 +70,85 @@ namespace Microsoft.AspNet.PipelineCore get { return IHttpRequest.Body; } set { IHttpRequest.Body = value; } } + + public override string Method + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override string Scheme + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override bool IsSecure + { + get { throw new NotImplementedException(); } + } + + public override HostString Host + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override IReadableStringCollection Query + { + get { throw new NotImplementedException(); } + } + + public override string Protocol + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override IHeaderDictionary Headers + { + get { throw new NotImplementedException(); } + } + + public override IReadableStringCollection Cookies + { + get { throw new NotImplementedException(); } + } + + public override System.Threading.CancellationToken CallCanceled + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.PipelineCore/Infrastructure/Constants.cs b/src/Microsoft.AspNet.PipelineCore/Infrastructure/Constants.cs new file mode 100644 index 0000000000..114ae660e3 --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/Infrastructure/Constants.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Abstractions.Infrastructure +{ + internal static class Constants + { + internal const string Https = "HTTPS"; + + internal const string HttpDateFormat = "r"; + + internal static class Headers + { + internal const string ContentType = "Content-Type"; + internal const string CacheControl = "Cache-Control"; + internal const string MediaType = "Media-Type"; + internal const string Accept = "Accept"; + internal const string Host = "Host"; + internal const string ETag = "ETag"; + internal const string Location = "Location"; + internal const string ContentLength = "Content-Length"; + internal const string SetCookie = "Set-Cookie"; + internal const string Expires = "Expires"; + } + } +} diff --git a/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs b/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs new file mode 100644 index 0000000000..204b3cda0e --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs @@ -0,0 +1,862 @@ +using Microsoft.AspNet.Abstractions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.PipelineCore.Infrastructure +{ + internal struct HeaderSegment : IEquatable + { + private readonly StringSegment _formatting; + private readonly StringSegment _data; + + // + // Initializes a new instance of the class. + // + public HeaderSegment(StringSegment formatting, StringSegment data) + { + _formatting = formatting; + _data = data; + } + + public StringSegment Formatting + { + get { return _formatting; } + } + + public StringSegment Data + { + get { return _data; } + } + + #region Equality members + + public bool Equals(HeaderSegment other) + { + return _formatting.Equals(other._formatting) && _data.Equals(other._data); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegment && Equals((HeaderSegment)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (_formatting.GetHashCode() * 397) ^ _data.GetHashCode(); + } + } + + public static bool operator ==(HeaderSegment left, HeaderSegment right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegment left, HeaderSegment right) + { + return !left.Equals(right); + } + + #endregion + } + + [System.CodeDom.Compiler.GeneratedCode("App_Packages", "")] + internal struct HeaderSegmentCollection : IEnumerable, IEquatable + { + private readonly string[] _headers; + + public HeaderSegmentCollection(string[] headers) + { + _headers = headers; + } + + #region Equality members + + public bool Equals(HeaderSegmentCollection other) + { + return Equals(_headers, other._headers); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegmentCollection && Equals((HeaderSegmentCollection)obj); + } + + public override int GetHashCode() + { + return (_headers != null ? _headers.GetHashCode() : 0); + } + + public static bool operator ==(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return !left.Equals(right); + } + + #endregion + + public Enumerator GetEnumerator() + { + return new Enumerator(_headers); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal struct Enumerator : IEnumerator + { + private readonly string[] _headers; + private int _index; + + private string _header; + private int _headerLength; + private int _offset; + + private int _leadingStart; + private int _leadingEnd; + private int _valueStart; + private int _valueEnd; + private int _trailingStart; + + private Mode _mode; + + private static readonly string[] NoHeaders = new string[0]; + + public Enumerator(string[] headers) + { + _headers = headers ?? NoHeaders; + _header = string.Empty; + _headerLength = -1; + _index = -1; + _offset = -1; + _leadingStart = -1; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + _mode = Mode.Leading; + } + + private enum Mode + { + Leading, + Value, + ValueQuoted, + Trailing, + Produce, + } + + private enum Attr + { + Value, + Quote, + Delimiter, + Whitespace + } + + public HeaderSegment Current + { + get + { + return new HeaderSegment( + new StringSegment(_header, _leadingStart, _leadingEnd - _leadingStart), + new StringSegment(_header, _valueStart, _valueEnd - _valueStart)); + } + } + + object IEnumerator.Current + { + get { return Current; } + } + + public void Dispose() + { + } + + public bool MoveNext() + { + while (true) + { + if (_mode == Mode.Produce) + { + _leadingStart = _trailingStart; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + if (_offset == _headerLength && + _leadingStart != -1 && + _leadingStart != _offset) + { + // Also produce trailing whitespace + _leadingEnd = _offset; + return true; + } + _mode = Mode.Leading; + } + + // if end of a string + if (_offset == _headerLength) + { + ++_index; + _offset = -1; + _leadingStart = 0; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + // if that was the last string + if (_index == _headers.Length) + { + // no more move nexts + return false; + } + + // grab the next string + _header = _headers[_index] ?? string.Empty; + _headerLength = _header.Length; + } + while (true) + { + ++_offset; + char ch = _offset == _headerLength ? (char)0 : _header[_offset]; + // todo - array of attrs + Attr attr = char.IsWhiteSpace(ch) ? Attr.Whitespace : ch == '\"' ? Attr.Quote : (ch == ',' || ch == (char)0) ? Attr.Delimiter : Attr.Value; + + switch (_mode) + { + case Mode.Leading: + switch (attr) + { + case Attr.Delimiter: + _leadingEnd = _offset; + _mode = Mode.Produce; + break; + case Attr.Quote: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Value: + switch (attr) + { + case Attr.Quote: + _mode = Mode.ValueQuoted; + break; + case Attr.Delimiter: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + break; + case Attr.Value: + // more + break; + case Attr.Whitespace: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Trailing; + break; + } + break; + case Mode.ValueQuoted: + switch (attr) + { + case Attr.Quote: + _mode = Mode.Value; + break; + case Attr.Delimiter: + if (ch == (char)0) + { + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + } + break; + case Attr.Value: + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Trailing: + switch (attr) + { + case Attr.Delimiter: + _mode = Mode.Produce; + break; + case Attr.Quote: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + } + if (_mode == Mode.Produce) + { + return true; + } + } + } + } + + public void Reset() + { + _index = 0; + _offset = 0; + _leadingStart = 0; + _leadingEnd = 0; + _valueStart = 0; + _valueEnd = 0; + } + } + } + + [System.CodeDom.Compiler.GeneratedCode("App_Packages", "")] + internal struct StringSegment : IEquatable + { + private readonly string _buffer; + private readonly int _offset; + private readonly int _count; + + // + // Initializes a new instance of the class. + // + public StringSegment(string buffer, int offset, int count) + { + _buffer = buffer; + _offset = offset; + _count = count; + } + + public string Buffer + { + get { return _buffer; } + } + + public int Offset + { + get { return _offset; } + } + + public int Count + { + get { return _count; } + } + + public string Value + { + get { return _offset == -1 ? null : _buffer.Substring(_offset, _count); } + } + + public bool HasValue + { + get { return _offset != -1 && _count != 0 && _buffer != null; } + } + + #region Equality members + + public bool Equals(StringSegment other) + { + return string.Equals(_buffer, other._buffer) && _offset == other._offset && _count == other._count; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is StringSegment && Equals((StringSegment)obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = (_buffer != null ? _buffer.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ _offset; + hashCode = (hashCode * 397) ^ _count; + return hashCode; + } + } + + public static bool operator ==(StringSegment left, StringSegment right) + { + return left.Equals(right); + } + + public static bool operator !=(StringSegment left, StringSegment right) + { + return !left.Equals(right); + } + + #endregion + + public bool StartsWith(string text, StringComparison comparisonType) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + int textLength = text.Length; + if (!HasValue || _count < textLength) + { + return false; + } + + return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0; + } + + public bool EndsWith(string text, StringComparison comparisonType) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + int textLength = text.Length; + if (!HasValue || _count < textLength) + { + return false; + } + + return string.Compare(_buffer, _offset + _count - textLength, text, 0, textLength, comparisonType) == 0; + } + + public bool Equals(string text, StringComparison comparisonType) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + int textLength = text.Length; + if (!HasValue || _count != textLength) + { + return false; + } + + return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0; + } + + public string Substring(int offset, int length) + { + return _buffer.Substring(_offset + offset, length); + } + + public StringSegment Subsegment(int offset, int length) + { + return new StringSegment(_buffer, _offset + offset, length); + } + + public override string ToString() + { + return Value ?? string.Empty; + } + } + + internal static partial class ParsingHelpers + { + private static readonly Action AddCookieCallback = (name, value, state) => + { + var dictionary = (IDictionary)state; + if (!dictionary.ContainsKey(name)) + { + dictionary.Add(name, value); + } + }; + + private static readonly char[] SemicolonAndComma = new[] { ';', ',' }; + + internal static IDictionary GetCookies(HttpRequest request) + { + var cookies = request.Get>("Microsoft.Owin.Cookies#dictionary"); + if (cookies == null) + { + cookies = new Dictionary(StringComparer.Ordinal); + request.Set("Microsoft.Owin.Cookies#dictionary", cookies); + } + + string text = GetHeader(request.Headers, "Cookie"); + if (request.Get("Microsoft.Owin.Cookies#text") != text) + { + cookies.Clear(); + ParseDelimited(text, SemicolonAndComma, AddCookieCallback, cookies); + request.Set("Microsoft.Owin.Cookies#text", text); + } + return cookies; + } + + internal static void ParseDelimited(string text, char[] delimiters, Action 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; + } + } + + internal static string GetJoinedValue(IDictionary Store, string key) + { + throw new NotImplementedException(); + } + } + + internal static partial class OwinHelpers + { + public static string GetHeader(IDictionary headers, string key) + { + string[] values = GetHeaderUnmodified(headers, key); + return values == null ? null : string.Join(",", values); + } + + public static IEnumerable GetHeaderSplit(IDictionary headers, string key) + { + string[] values = GetHeaderUnmodified(headers, key); + return values == null ? null : GetHeaderSplitImplementation(values); + } + + private static IEnumerable GetHeaderSplitImplementation(string[] values) + { + foreach (var segment in new HeaderSegmentCollection(values)) + { + if (segment.Data.HasValue) + { + yield return DeQuote(segment.Data.Value); + } + } + } + + public static string[] GetHeaderUnmodified(IDictionary 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 headers, string key, string value) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException("key"); + } + if (string.IsNullOrWhiteSpace(value)) + { + headers.Remove(key); + } + else + { + headers[key] = new[] { value }; + } + } + + public static void SetHeaderJoined(IDictionary headers, string key, params string[] values) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException("key"); + } + if (values == null || values.Length == 0) + { + headers.Remove(key); + } + else + { + headers[key] = new[] { string.Join(",", values.Select(value => QuoteIfNeeded(value))) }; + } + } + + // Quote items that contain comas and are not already quoted. + private static string QuoteIfNeeded(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + // Ignore + } + else if (value.Contains(',')) + { + if (value[0] != '"' || value[value.Length - 1] != '"') + { + value = '"' + value + '"'; + } + } + + return value; + } + + private static string DeQuote(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + // Ignore + } + else if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"') + { + value = value.Substring(1, value.Length - 2); + } + + return value; + } + + public static void SetHeaderUnmodified(IDictionary headers, string key, params string[] values) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException("key"); + } + if (values == null || values.Length == 0) + { + headers.Remove(key); + } + else + { + headers[key] = values; + } + } + + public static void SetHeaderUnmodified(IDictionary headers, string key, IEnumerable values) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + headers[key] = values.ToArray(); + } + + public static void AppendHeader(IDictionary headers, string key, string values) + { + if (string.IsNullOrWhiteSpace(values)) + { + return; + } + + string existing = GetHeader(headers, key); + if (existing == null) + { + SetHeader(headers, key, values); + } + else + { + headers[key] = new[] { existing + "," + values }; + } + } + + public static void AppendHeaderJoined(IDictionary headers, string key, params string[] values) + { + if (values == null || values.Length == 0) + { + return; + } + + string existing = GetHeader(headers, key); + if (existing == null) + { + SetHeaderJoined(headers, key, values); + } + else + { + headers[key] = new[] { existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))) }; + } + } + + public static void AppendHeaderUnmodified(IDictionary headers, string key, params string[] values) + { + if (values == null || values.Length == 0) + { + return; + } + + string[] existing = GetHeaderUnmodified(headers, key); + if (existing == null) + { + SetHeaderUnmodified(headers, key, values); + } + else + { + SetHeaderUnmodified(headers, key, existing.Concat(values)); + } + } + } + + internal static partial class OwinHelpers + { + private static readonly Action AppendItemCallback = (name, value, state) => + { + var dictionary = (IDictionary>)state; + + List existing; + if (!dictionary.TryGetValue(name, out existing)) + { + dictionary.Add(name, new List(1) { value }); + } + else + { + existing.Add(value); + } + }; + + private static readonly char[] AmpersandAndSemicolon = new[] { '&', ';' }; + + internal static IDictionary GetQuery(HttpRequest request) + { + var query = request.Get>("Microsoft.Owin.Query#dictionary"); + if (query == null) + { + query = new Dictionary(StringComparer.OrdinalIgnoreCase); + request.Set("Microsoft.Owin.Query#dictionary", query); + } + + string text = request.QueryString.Value; + if (request.Get("Microsoft.Owin.Query#text") != text) + { + query.Clear(); + var accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); + ParseDelimited(text, AmpersandAndSemicolon, AppendItemCallback, accumulator); + foreach (var kv in accumulator) + { + query.Add(kv.Key, kv.Value.ToArray()); + } + request.Set("Microsoft.Owin.Query#text", text); + } + return query; + } + +#if !NET40 + internal static IFormCollection GetForm(string text) + { + IDictionary form = new Dictionary(StringComparer.OrdinalIgnoreCase); + var accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); + ParseDelimited(text, new[] { '&' }, AppendItemCallback, accumulator); + foreach (var kv in accumulator) + { + form.Add(kv.Key, kv.Value.ToArray()); + } + return new FormCollection(form); + } +#endif + + internal static string GetJoinedValue(IDictionary store, string key) + { + string[] values = GetUnmodifiedValues(store, key); + return values == null ? null : string.Join(",", values); + } + + internal static string[] GetUnmodifiedValues(IDictionary store, string key) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + string[] values; + return store.TryGetValue(key, out values) ? values : null; + } + } + + internal static partial class OwinHelpers + { + internal static string GetHost(HttpRequest request) + { + IHeaderDictionary headers = request.Headers; + + string host = GetHeader(headers, "Host"); + if (!string.IsNullOrWhiteSpace(host)) + { + return host; + } + + string localIpAddress = request.LocalIpAddress ?? "localhost"; + var localPort = request.Get(OwinConstants.CommonKeys.LocalPort); + return string.IsNullOrWhiteSpace(localPort) ? localIpAddress : (localIpAddress + ":" + localPort); + } + } +} diff --git a/test/Microsoft.AspNet.FeatureModel.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.AspNet.FeatureModel.Tests/Properties/AssemblyInfo.cs index da65ff9698..8beefe2634 100644 --- a/test/Microsoft.AspNet.FeatureModel.Tests/Properties/AssemblyInfo.cs +++ b/test/Microsoft.AspNet.FeatureModel.Tests/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -31,6 +31,6 @@ using System.Runtime.InteropServices; // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +// [assembly: AssemblyVersion("0.1.0")] +[assembly: AssemblyVersion("0.1.0")] +[assembly: AssemblyFileVersion("0.1.0")] diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/Microsoft.AspNet.PipelineCore.Tests.csproj b/test/Microsoft.AspNet.PipelineCore.Tests/Microsoft.AspNet.PipelineCore.Tests.csproj index 9ef35f3c8d..d68b62764a 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/Microsoft.AspNet.PipelineCore.Tests.csproj +++ b/test/Microsoft.AspNet.PipelineCore.Tests/Microsoft.AspNet.PipelineCore.Tests.csproj @@ -53,6 +53,19 @@ + + + + + + + + + + + + +