This repository has been archived by the owner on Dec 14, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Fixes #5212] Added a cookie based ITempDataProvider
- Loading branch information
Showing
9 changed files
with
552 additions
and
216 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
233 changes: 233 additions & 0 deletions
233
src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataProviderStore.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Reflection; | ||
using Microsoft.Extensions.Internal; | ||
using Newtonsoft.Json; | ||
using Newtonsoft.Json.Bson; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal | ||
{ | ||
public class TempDataProviderStore | ||
{ | ||
private readonly JsonSerializer _jsonSerializer = JsonSerializer.Create( | ||
new JsonSerializerSettings() | ||
{ | ||
TypeNameHandling = TypeNameHandling.None | ||
}); | ||
|
||
private static readonly MethodInfo _convertArrayMethodInfo = typeof(TempDataProviderStore).GetMethod( | ||
nameof(ConvertArray), BindingFlags.Static | BindingFlags.NonPublic); | ||
private static readonly MethodInfo _convertDictMethodInfo = typeof(TempDataProviderStore).GetMethod( | ||
nameof(ConvertDictionary), BindingFlags.Static | BindingFlags.NonPublic); | ||
|
||
private static readonly ConcurrentDictionary<Type, Func<JArray, object>> _arrayConverters = | ||
new ConcurrentDictionary<Type, Func<JArray, object>>(); | ||
private static readonly ConcurrentDictionary<Type, Func<JObject, object>> _dictionaryConverters = | ||
new ConcurrentDictionary<Type, Func<JObject, object>>(); | ||
|
||
private static readonly Dictionary<JTokenType, Type> _tokenTypeLookup = new Dictionary<JTokenType, Type> | ||
{ | ||
{ JTokenType.String, typeof(string) }, | ||
{ JTokenType.Integer, typeof(int) }, | ||
{ JTokenType.Boolean, typeof(bool) }, | ||
{ JTokenType.Float, typeof(float) }, | ||
{ JTokenType.Guid, typeof(Guid) }, | ||
{ JTokenType.Date, typeof(DateTime) }, | ||
{ JTokenType.TimeSpan, typeof(TimeSpan) }, | ||
{ JTokenType.Uri, typeof(Uri) }, | ||
}; | ||
|
||
public IDictionary<string, object> DeserializeTempData(byte[] value) | ||
{ | ||
var tempDataDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); | ||
using (var memoryStream = new MemoryStream(value)) | ||
using (var writer = new BsonReader(memoryStream)) | ||
{ | ||
tempDataDictionary = _jsonSerializer.Deserialize<Dictionary<string, object>>(writer); | ||
} | ||
|
||
var convertedDictionary = new Dictionary<string, object>( | ||
tempDataDictionary, | ||
StringComparer.OrdinalIgnoreCase); | ||
foreach (var item in tempDataDictionary) | ||
{ | ||
var jArrayValue = item.Value as JArray; | ||
var jObjectValue = item.Value as JObject; | ||
if (jArrayValue != null && jArrayValue.Count > 0) | ||
{ | ||
var arrayType = jArrayValue[0].Type; | ||
Type returnType; | ||
if (_tokenTypeLookup.TryGetValue(arrayType, out returnType)) | ||
{ | ||
var arrayConverter = _arrayConverters.GetOrAdd(returnType, type => | ||
{ | ||
return (Func<JArray, object>)_convertArrayMethodInfo | ||
.MakeGenericMethod(type) | ||
.CreateDelegate(typeof(Func<JArray, object>)); | ||
}); | ||
var result = arrayConverter(jArrayValue); | ||
|
||
convertedDictionary[item.Key] = result; | ||
} | ||
else | ||
{ | ||
var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), arrayType); | ||
throw new InvalidOperationException(message); | ||
} | ||
} | ||
else if (jObjectValue != null) | ||
{ | ||
if (!jObjectValue.HasValues) | ||
{ | ||
convertedDictionary[item.Key] = null; | ||
continue; | ||
} | ||
|
||
var jTokenType = jObjectValue.Properties().First().Value.Type; | ||
Type valueType; | ||
if (_tokenTypeLookup.TryGetValue(jTokenType, out valueType)) | ||
{ | ||
var dictionaryConverter = _dictionaryConverters.GetOrAdd(valueType, type => | ||
{ | ||
return (Func<JObject, object>)_convertDictMethodInfo | ||
.MakeGenericMethod(type) | ||
.CreateDelegate(typeof(Func<JObject, object>)); | ||
}); | ||
var result = dictionaryConverter(jObjectValue); | ||
|
||
convertedDictionary[item.Key] = result; | ||
} | ||
else | ||
{ | ||
var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), jTokenType); | ||
throw new InvalidOperationException(message); | ||
} | ||
} | ||
else if (item.Value is long) | ||
{ | ||
var longValue = (long)item.Value; | ||
if (longValue >= int.MinValue && longValue <= int.MaxValue) | ||
{ | ||
// BsonReader casts all ints to longs. We'll attempt to work around this by force converting | ||
// longs to ints when there's no loss of precision. | ||
convertedDictionary[item.Key] = (int)longValue; | ||
} | ||
} | ||
} | ||
|
||
return convertedDictionary; | ||
} | ||
|
||
public byte[] SerializeTempData(IDictionary<string, object> values) | ||
{ | ||
var hasValues = (values != null && values.Count > 0); | ||
if (hasValues) | ||
{ | ||
foreach (var item in values.Values) | ||
{ | ||
// We want to allow only simple types to be serialized. | ||
EnsureObjectCanBeSerialized(item); | ||
} | ||
|
||
using (var memoryStream = new MemoryStream()) | ||
{ | ||
using (var writer = new BsonWriter(memoryStream)) | ||
{ | ||
_jsonSerializer.Serialize(writer, values); | ||
return memoryStream.ToArray(); | ||
} | ||
} | ||
} | ||
else | ||
{ | ||
return new byte[0]; | ||
} | ||
} | ||
|
||
internal void EnsureObjectCanBeSerialized(object item) | ||
{ | ||
var itemType = item.GetType(); | ||
Type actualType = null; | ||
|
||
if (itemType.IsArray) | ||
{ | ||
itemType = itemType.GetElementType(); | ||
} | ||
else if (itemType.GetTypeInfo().IsGenericType) | ||
{ | ||
if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IList<>)) != null) | ||
{ | ||
var genericTypeArguments = itemType.GenericTypeArguments; | ||
Debug.Assert(genericTypeArguments.Length == 1, "IList<T> has one generic argument"); | ||
actualType = genericTypeArguments[0]; | ||
} | ||
else if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IDictionary<,>)) != null) | ||
{ | ||
var genericTypeArguments = itemType.GenericTypeArguments; | ||
Debug.Assert( | ||
genericTypeArguments.Length == 2, | ||
"IDictionary<TKey, TValue> has two generic arguments"); | ||
|
||
// Throw if the key type of the dictionary is not string. | ||
if (genericTypeArguments[0] != typeof(string)) | ||
{ | ||
var message = Resources.FormatTempData_CannotSerializeDictionary( | ||
typeof(TempDataProviderStore).FullName, genericTypeArguments[0]); | ||
throw new InvalidOperationException(message); | ||
} | ||
else | ||
{ | ||
actualType = genericTypeArguments[1]; | ||
} | ||
} | ||
} | ||
|
||
actualType = actualType ?? itemType; | ||
if (!IsSimpleType(actualType)) | ||
{ | ||
var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType; | ||
var message = Resources.FormatTempData_CannotSerializeToSession( | ||
typeof(TempDataProviderStore).FullName, underlyingType); | ||
throw new InvalidOperationException(message); | ||
} | ||
} | ||
|
||
private static IList<TVal> ConvertArray<TVal>(JArray array) | ||
{ | ||
return array.Values<TVal>().ToArray(); | ||
} | ||
|
||
private static IDictionary<string, TVal> ConvertDictionary<TVal>(JObject jObject) | ||
{ | ||
var convertedDictionary = new Dictionary<string, TVal>(StringComparer.Ordinal); | ||
foreach (var item in jObject) | ||
{ | ||
convertedDictionary.Add(item.Key, jObject.Value<TVal>(item.Key)); | ||
} | ||
return convertedDictionary; | ||
} | ||
|
||
private static bool IsSimpleType(Type type) | ||
{ | ||
var typeInfo = type.GetTypeInfo(); | ||
|
||
return typeInfo.IsPrimitive || | ||
typeInfo.IsEnum || | ||
type.Equals(typeof(decimal)) || | ||
type.Equals(typeof(string)) || | ||
type.Equals(typeof(DateTime)) || | ||
type.Equals(typeof(Guid)) || | ||
type.Equals(typeof(DateTimeOffset)) || | ||
type.Equals(typeof(TimeSpan)) || | ||
type.Equals(typeof(Uri)); | ||
} | ||
} | ||
} |
78 changes: 78 additions & 0 deletions
78
src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/CookieTempDataProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using Microsoft.AspNetCore.DataProtection; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures | ||
{ | ||
public class CookieTempDataProvider : ITempDataProvider | ||
{ | ||
public static readonly string CookieName = ".AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider"; | ||
|
||
//TODO: DOES THE LENGTH OF THIS NAME MATTER? | ||
private static readonly string Purpose = "Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProviderToken.v1"; | ||
private const byte TokenVersion = 0x01; | ||
private readonly IDataProtector _cryptoSystem; | ||
private readonly IDataProtector _dataProtector; | ||
private TempDataProviderStore _tempDataProviderStore; | ||
|
||
public CookieTempDataProvider(IDataProtectionProvider dataProtectionProvider) | ||
{ | ||
_cryptoSystem = dataProtectionProvider.CreateProtector(Purpose); | ||
_dataProtector = _cryptoSystem.CreateProtector(Purpose); | ||
_tempDataProviderStore = new TempDataProviderStore(); | ||
} | ||
|
||
public IDictionary<string, object> LoadTempData(HttpContext context) | ||
{ | ||
if (context == null) | ||
{ | ||
throw new ArgumentNullException(nameof(context)); | ||
} | ||
|
||
string value; | ||
if (context.Request.Cookies.TryGetValue(CookieName, out value)) | ||
{ | ||
var base64EncodedValue = Convert.FromBase64String(value); | ||
var unprotectedData = _dataProtector.Unprotect(base64EncodedValue); | ||
return _tempDataProviderStore.DeserializeTempData(unprotectedData); | ||
} | ||
|
||
return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); | ||
} | ||
|
||
public void SaveTempData(HttpContext context, IDictionary<string, object> values) | ||
{ | ||
if (context == null) | ||
{ | ||
throw new ArgumentNullException(nameof(context)); | ||
} | ||
|
||
var hasValues = (values != null && values.Count > 0); | ||
if (hasValues) | ||
{ | ||
var bytes = _tempDataProviderStore.SerializeTempData(values); | ||
bytes = _dataProtector.Protect(bytes); | ||
|
||
context.Response.Cookies.Append( | ||
CookieName, | ||
Convert.ToBase64String(bytes), | ||
new CookieOptions() | ||
{ | ||
//Expires TODO: THE TIME REQUIRED | ||
Path = context.Request.PathBase, | ||
HttpOnly = true, | ||
Secure = true | ||
}); | ||
} | ||
else | ||
{ | ||
context.Response.Cookies.Delete(CookieName); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.