| File: Internal\ResponseCookies.cs | Web Access |
| Project: src\aspnetcore\src\Http\Http\src\Microsoft.AspNetCore.Http.csproj (Microsoft.AspNetCore.Http) |
// 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.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Http; /// <summary> /// A wrapper for the response Set-Cookie header. /// </summary> internal sealed partial class ResponseCookies : IResponseCookies { private readonly IFeatureCollection _features; private ILogger? _logger; /// <summary> /// Create a new wrapper. /// </summary> internal ResponseCookies(IFeatureCollection features) { _features = features; Headers = _features.GetRequiredFeature<IHttpResponseFeature>().Headers; } private IHeaderDictionary Headers { get; set; } /// <inheritdoc /> public void Append(string key, string value) { var setCookieHeaderValue = new SetCookieHeaderValue(key, Uri.EscapeDataString(value)) { Path = "/" }; var cookieValue = setCookieHeaderValue.ToString(); Headers.SetCookie = StringValues.Concat(Headers.SetCookie, cookieValue); } /// <inheritdoc /> public void Append(string key, string value, CookieOptions options) { ArgumentNullException.ThrowIfNull(options); // SameSite=None cookies must be marked as Secure. if (!options.Secure && options.SameSite == SameSiteMode.None) { if (_logger == null) { var services = _features.Get<IServiceProvidersFeature>()?.RequestServices; _logger = services?.GetService<ILogger<ResponseCookies>>(); } if (_logger != null) { Log.SameSiteCookieNotSecure(_logger, key); } } var cookie = options.CreateCookieHeader(key, Uri.EscapeDataString(value)).ToString(); Headers.SetCookie = StringValues.Concat(Headers.SetCookie, cookie); } /// <inheritdoc /> public void Append(ReadOnlySpan<KeyValuePair<string, string>> keyValuePairs, CookieOptions options) { ArgumentNullException.ThrowIfNull(options); // SameSite=None cookies must be marked as Secure. if (!options.Secure && options.SameSite == SameSiteMode.None) { if (_logger == null) { var services = _features.Get<IServiceProvidersFeature>()?.RequestServices; _logger = services?.GetService<ILogger<ResponseCookies>>(); } if (_logger != null) { foreach (var keyValuePair in keyValuePairs) { Log.SameSiteCookieNotSecure(_logger, keyValuePair.Key); } } } var cookieSuffix = options.CreateCookieHeader(string.Empty, string.Empty).ToString().AsSpan(1); var cookies = new string[keyValuePairs.Length]; var position = 0; foreach (var keyValuePair in keyValuePairs) { cookies[position] = string.Concat(keyValuePair.Key, "=", Uri.EscapeDataString(keyValuePair.Value), cookieSuffix); position++; } // Can't use += as StringValues does not override operator+ // and the implicit conversions will cause an incorrect string concat https://github.com/dotnet/runtime/issues/52507 Headers.SetCookie = StringValues.Concat(Headers.SetCookie, cookies); } /// <inheritdoc /> public void Delete(string key) { Delete(key, new CookieOptions() { Path = "/" }); } /// <inheritdoc /> public void Delete(string key, CookieOptions options) { ArgumentNullException.ThrowIfNull(options); var encodedKeyPlusEquals = key + "="; var domainHasValue = !string.IsNullOrEmpty(options.Domain); var pathHasValue = !string.IsNullOrEmpty(options.Path); Func<string, string, CookieOptions, bool> rejectPredicate; if (domainHasValue && pathHasValue) { rejectPredicate = (value, encKeyPlusEquals, opts) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && value.Contains($"domain={opts.Domain}", StringComparison.OrdinalIgnoreCase) && value.Contains($"path={opts.Path}", StringComparison.OrdinalIgnoreCase); } else if (domainHasValue) { rejectPredicate = (value, encKeyPlusEquals, opts) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && value.Contains($"domain={opts.Domain}", StringComparison.OrdinalIgnoreCase); } else if (pathHasValue) { rejectPredicate = (value, encKeyPlusEquals, opts) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && value.Contains($"path={opts.Path}", StringComparison.OrdinalIgnoreCase); } else { rejectPredicate = (value, encKeyPlusEquals, opts) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase); } var existingValues = Headers.SetCookie; if (!StringValues.IsNullOrEmpty(existingValues)) { var values = existingValues.ToArray(); var newValues = new List<string>(); for (var i = 0; i < values.Length; i++) { var value = values[i] ?? string.Empty; if (!rejectPredicate(value, encodedKeyPlusEquals, options)) { newValues.Add(value); } } Headers.SetCookie = new StringValues(newValues.ToArray()); } Append(key, string.Empty, new CookieOptions(options) { Expires = DateTimeOffset.UnixEpoch, MaxAge = null, // Some browsers require this (https://github.com/dotnet/aspnetcore/issues/52159) }); } private static partial class Log { [LoggerMessage(1, LogLevel.Warning, "The cookie '{name}' has set 'SameSite=None' and must also set 'Secure'.", EventName = "SameSiteNotSecure")] public static partial void SameSiteCookieNotSecure(ILogger logger, string name); } }