|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
/// <summary>
/// Provides data from cookie to the current <see cref="ITempDataDictionary"/> object.
/// </summary>
public partial class CookieTempDataProvider : ITempDataProvider
{
/// <summary>
/// The name of the cookie.
/// </summary>
public static readonly string CookieName = ".AspNetCore.Mvc.CookieTempDataProvider";
private const string Purpose = "Microsoft.AspNetCore.Mvc.CookieTempDataProviderToken.v1";
private readonly IDataProtector _dataProtector;
private readonly ILogger _logger;
private readonly TempDataSerializer _tempDataSerializer;
private readonly ChunkingCookieManager _chunkingCookieManager;
private readonly CookieTempDataProviderOptions _options;
/// <summary>
/// Initializes a new instance of <see cref="CookieTempDataProvider"/>.
/// </summary>
/// <param name="dataProtectionProvider">The <see cref="IDataProtectionProvider"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="options">The <see cref="CookieTempDataProviderOptions"/>.</param>
/// <param name="tempDataSerializer">The <see cref="TempDataSerializer"/>.</param>
public CookieTempDataProvider(
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
IOptions<CookieTempDataProviderOptions> options,
TempDataSerializer tempDataSerializer)
{
_dataProtector = dataProtectionProvider.CreateProtector(Purpose);
_logger = loggerFactory.CreateLogger<CookieTempDataProvider>();
_tempDataSerializer = tempDataSerializer;
_chunkingCookieManager = new ChunkingCookieManager();
_options = options.Value;
}
/// <summary>
/// Loads the temp data from the request.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <returns>The temp data.</returns>
public IDictionary<string, object> LoadTempData(HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
if (context.Request.Cookies.ContainsKey(_options.Cookie.Name))
{
// The cookie we use for temp data is user input, and might be invalid in many ways.
//
// Since TempData is a best-effort system, we don't want to throw and get a 500 if the cookie is
// bad, we will just clear it and ignore the exception. The common case that we've identified for
// this is misconfigured data protection settings, which can cause the key used to create the
// cookie to no longer be available.
try
{
var encodedValue = _chunkingCookieManager.GetRequestCookie(context, _options.Cookie.Name);
if (!string.IsNullOrEmpty(encodedValue))
{
var protectedData = WebEncoders.Base64UrlDecode(encodedValue);
var unprotectedData = _dataProtector.Unprotect(protectedData);
var tempData = _tempDataSerializer.Deserialize(unprotectedData);
Log.TempDataCookieLoadSuccess(_logger, _options.Cookie.Name);
return tempData;
}
}
catch (Exception ex)
{
Log.TempDataCookieLoadFailure(_logger, _options.Cookie.Name, ex);
// If we've failed, we want to try and clear the cookie so that this won't keep happening
// over and over.
if (!context.Response.HasStarted)
{
_chunkingCookieManager.DeleteCookie(context, _options.Cookie.Name, _options.Cookie.Build(context));
}
}
}
Log.TempDataCookieNotFound(_logger, _options.Cookie.Name);
return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Save the temp data to the request.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="values">The values.</param>
public void SaveTempData(HttpContext context, IDictionary<string, object> values)
{
ArgumentNullException.ThrowIfNull(context);
var cookieOptions = _options.Cookie.Build(context);
SetCookiePath(context, cookieOptions);
var hasValues = (values != null && values.Count > 0);
if (hasValues)
{
var bytes = _tempDataSerializer.Serialize(values);
bytes = _dataProtector.Protect(bytes);
var encodedValue = WebEncoders.Base64UrlEncode(bytes);
_chunkingCookieManager.AppendResponseCookie(context, _options.Cookie.Name, encodedValue, cookieOptions);
}
else
{
_chunkingCookieManager.DeleteCookie(context, _options.Cookie.Name, cookieOptions);
}
}
private void SetCookiePath(HttpContext httpContext, CookieOptions cookieOptions)
{
if (!string.IsNullOrEmpty(_options.Cookie.Path))
{
cookieOptions.Path = _options.Cookie.Path;
}
else
{
var pathBase = httpContext.Request.PathBase.ToString();
if (!string.IsNullOrEmpty(pathBase))
{
cookieOptions.Path = pathBase;
}
}
}
private static partial class Log
{
[LoggerMessage(1, LogLevel.Debug, "The temp data cookie {CookieName} was not found.", EventName = "TempDataCookieNotFound")]
public static partial void TempDataCookieNotFound(ILogger logger, string cookieName);
[LoggerMessage(2, LogLevel.Debug, "The temp data cookie {CookieName} was used to successfully load temp data.", EventName = "TempDataCookieLoadSuccess")]
public static partial void TempDataCookieLoadSuccess(ILogger logger, string cookieName);
[LoggerMessage(3, LogLevel.Warning, "The temp data cookie {CookieName} could not be loaded.", EventName = "TempDataCookieLoadFailure")]
public static partial void TempDataCookieLoadFailure(ILogger logger, string cookieName, Exception exception);
}
}
|