|
//------------------------------------------------------------------------------
// <copyright file="HttpCookie.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
/*
* HttpCookie - collection + name + path
*
* Copyright (c) 1998 Microsoft Corporation
*/
namespace System.Web {
using System.Text;
using System.Collections;
using System.Collections.Specialized;
using System.Globalization;
using System.Security.Permissions;
using System.Web.Configuration;
using System.Web.Management;
using Util;
using System.ComponentModel;
/// <devdoc>
/// <para>
/// Provides a type-safe way
/// to access multiple HTTP cookies.
/// </para>
/// </devdoc>
public sealed class HttpCookie {
private String _name;
private String _path = "/";
private bool _secure;
private bool _httpOnly;
private String _domain;
private bool _expirationSet;
private DateTime _expires;
private String _stringValue;
private HttpValueCollection _multiValue;
private bool _changed;
private bool _added;
private SameSiteMode _sameSite;
internal HttpCookie() {
_changed = true;
}
/*
* Constructor - empty cookie with name
*/
/// <devdoc>
/// <para>
/// Initializes a new instance of the <see cref='System.Web.HttpCookie'/>
/// class.
/// </para>
/// </devdoc>
public HttpCookie(String name) {
_name = name;
SetDefaultsFromConfig();
_changed = true;
}
/*
* Constructor - cookie with name and value
*/
/// <devdoc>
/// <para>
/// Initializes a new instance of the <see cref='System.Web.HttpCookie'/>
/// class.
/// </para>
/// </devdoc>
public HttpCookie(String name, String value) {
_name = name;
_stringValue = value;
SetDefaultsFromConfig();
_changed = true;
}
private void SetDefaultsFromConfig() {
HttpCookiesSection config = RuntimeConfig.GetConfig().HttpCookies;
_secure = config.RequireSSL;
_httpOnly = config.HttpOnlyCookies;
_sameSite = config.SameSite;
if (config.Domain != null && config.Domain.Length > 0)
_domain = config.Domain;
}
/*
* Whether the cookie contents have changed
*/
internal bool Changed {
get { return _changed; }
set { _changed = value; }
}
/*
* Whether the cookie has been added
*/
internal bool Added {
get { return _added; }
set { _added = value; }
}
// DevID 251951 Cookie is getting duplicated by ASP.NET when they are added via a native module
// This flag is used to remember that this cookie came from an IIS Set-Header flag,
// so we don't duplicate it and send it back to IIS
internal bool IsInResponseHeader {
get;
set;
}
/*
* Cookie name
*/
/// <devdoc>
/// <para>
/// Gets
/// or sets the name of cookie.
/// </para>
/// </devdoc>
public String Name {
get { return _name;}
set {
_name = value;
_changed = true;
}
}
/*
* Cookie path
*/
/// <devdoc>
/// <para>
/// Gets or sets the URL prefix to transmit with the
/// current cookie.
/// </para>
/// </devdoc>
public String Path {
get { return _path;}
set {
_path = value;
_changed = true;
}
}
/*
* 'Secure' flag
*/
/// <devdoc>
/// <para>
/// Indicates whether the cookie should be transmitted only over HTTPS.
/// </para>
/// </devdoc>
public bool Secure {
get { return _secure;}
set {
_secure = value;
_changed = true;
}
}
/// <summary>
/// Determines whether this cookie is allowed to participate in output caching.
/// </summary>
/// <remarks>
/// If a given HttpResponse contains one or more outbound cookies with Shareable = false (the default value),
/// output caching will be suppressed for that response. This prevents cookies that contain potentially
/// sensitive information, e.g. FormsAuth cookies, from being cached in the response and sent to multiple
/// clients. If a developer wants to allow a response containing cookies to be cached, he should configure
/// caching as normal for the response, e.g. via the OutputCache directive, MVC's [OutputCache] attribute,
/// etc., and he should make sure that all outbound cookies are marked Shareable = true.
/// </remarks>
public bool Shareable {
get;
set; // don't need to set _changed flag since Set-Cookie header isn't affected by value of Shareable
}
/// <devdoc>
/// <para>
/// Indicates whether the cookie should have HttpOnly attribute
/// </para>
/// </devdoc>
public bool HttpOnly {
get { return _httpOnly;}
set {
_httpOnly = value;
_changed = true;
}
}
/*
* Cookie domain
*/
/// <devdoc>
/// <para>
/// Restricts domain cookie is to be used with.
/// </para>
/// </devdoc>
public String Domain {
get { return _domain;}
set {
_domain = value;
_changed = true;
}
}
/*
* Cookie expiration
*/
/// <devdoc>
/// <para>
/// Expiration time for cookie (in minutes).
/// </para>
/// </devdoc>
public DateTime Expires {
get {
return(_expirationSet ? _expires : DateTime.MinValue);
}
set {
_expires = value;
_expirationSet = true;
_changed = true;
}
}
/*
* Cookie value as string
*/
/// <devdoc>
/// <para>
/// Gets
/// or
/// sets an individual cookie value.
/// </para>
/// </devdoc>
public String Value {
get {
if (_multiValue != null)
return _multiValue.ToString(false);
else
return _stringValue;
}
set {
if (_multiValue != null) {
// reset multivalue collection to contain
// single keyless value
_multiValue.Reset();
_multiValue.Add(null, value);
}
else {
// remember as string
_stringValue = value;
}
_changed = true;
}
}
/// <devdoc>
/// <para>SameSite mode for the current cookie</para>
/// </devdoc>
public SameSiteMode SameSite {
get {
return _sameSite;
}
set {
_sameSite = value;
_changed = true;
}
}
/*
* Checks is cookie has sub-keys
*/
/// <devdoc>
/// <para>Gets a
/// value indicating whether the cookie has sub-keys.</para>
/// </devdoc>
public bool HasKeys {
get { return Values.HasKeys();}
}
private bool SupportsHttpOnly(HttpContext context) {
if (context != null && context.Request != null) {
HttpBrowserCapabilities browser = context.Request.Browser;
return (browser != null && (browser.Type != "IE5" || browser.Platform != "MacPPC"));
}
return false;
}
/*
* Cookie values as multivalue collection
*/
/// <devdoc>
/// <para>Gets individual key:value pairs within a single cookie object.</para>
/// </devdoc>
public NameValueCollection Values {
get {
if (_multiValue == null) {
// create collection on demand
_multiValue = new HttpValueCollection();
// convert existing string value into multivalue
if (_stringValue != null) {
if (_stringValue.IndexOf('&') >= 0 || _stringValue.IndexOf('=') >= 0)
_multiValue.FillFromString(_stringValue);
else
_multiValue.Add(null, _stringValue);
_stringValue = null;
}
}
_changed = true;
return _multiValue;
}
}
/*
* Default indexed property -- lookup the multivalue collection
*/
/// <devdoc>
/// <para>
/// Shortcut for HttpCookie$Values[key]. Required for ASP compatibility.
/// </para>
/// </devdoc>
public String this[String key]
{
get {
return Values[key];
}
set {
Values[key] = value;
_changed = true;
}
}
/// <summary>
/// Converts the specified string representation of an HTTP cookie to HttpCookie
/// </summary>
/// <param name="input"></param>
/// <param name="result"></param>
/// <returns></returns>
public static bool TryParse(string input, out HttpCookie result) {
result = null;
if (string.IsNullOrEmpty(input)) {
return false;
}
// The substring before the first ';' is cookie-pair, with format of cookiename[=key1=val2&key2=val2&...]
int dividerIndex = input.IndexOf(';');
string cookiePair = dividerIndex >= 0 ? input.Substring(0, dividerIndex) : input;
HttpCookie cookie = HttpRequest.CreateCookieFromString(cookiePair.Trim());
// If there was no cookie name being created, stop parsing and return
if (string.IsNullOrEmpty(cookie.Name)) {
return false;
}
//
// Parse the collections of cookie-av
// cookie-av = expires-av/max-age-av/domain-av/path-av/secure-av/httponly-av/extension-av
// https://tools.ietf.org/html/rfc6265
while (dividerIndex >= 0 && dividerIndex < input.Length - 1) {
int cookieAvStartIndex = dividerIndex + 1;
dividerIndex = input.IndexOf(';', cookieAvStartIndex);
string cookieAv = dividerIndex >= 0 ? input.Substring(cookieAvStartIndex, dividerIndex - cookieAvStartIndex).Trim() : input.Substring(cookieAvStartIndex).Trim();
int assignmentIndex = cookieAv.IndexOf('=');
string attributeName = assignmentIndex >= 0 ? cookieAv.Substring(0, assignmentIndex).Trim() : cookieAv;
string attributeValue = assignmentIndex >= 0 && assignmentIndex < cookieAv.Length - 1 ? cookieAv.Substring(assignmentIndex + 1).Trim() : null;
//
// Parse supported cookie-av Attribute
//
// Expires
if (StringUtil.EqualsIgnoreCase(attributeName, "Expires")) {
DateTime dt;
if (DateTime.TryParse(attributeValue, out dt)) {
cookie.Expires = dt;
}
}
//
// Domain
else if (attributeValue != null && StringUtil.EqualsIgnoreCase(attributeName, "Domain")) {
cookie.Domain = attributeValue;
}
//
// Path
else if (attributeValue != null && StringUtil.EqualsIgnoreCase(attributeName, "Path")) {
cookie.Path = attributeValue;
}
//
// Secure
else if (StringUtil.EqualsIgnoreCase(attributeName, "Secure")) {
cookie.Secure = true;
}
//
// HttpOnly
else if (StringUtil.EqualsIgnoreCase(attributeName, "HttpOnly")) {
cookie.HttpOnly = true;
}
//
// SameSite
else if(StringUtil.EqualsIgnoreCase(attributeName, "SameSite")) {
SameSiteMode sameSite = (SameSiteMode)(-1);
if(Enum.TryParse<SameSiteMode>(attributeValue, true, out sameSite)) {
cookie.SameSite = sameSite;
}
}
}
result = cookie;
return true;
}
/*
* Construct set-cookie header
*/
internal HttpResponseHeader GetSetCookieHeader(HttpContext context) {
StringBuilder s = new StringBuilder();
// cookiename=
if (!String.IsNullOrEmpty(_name)) {
s.Append(_name);
s.Append('=');
}
// key=value&...
if (_multiValue != null)
s.Append(_multiValue.ToString(false));
else if (_stringValue != null)
s.Append(_stringValue);
// domain
if (!String.IsNullOrEmpty(_domain)) {
s.Append("; domain=");
s.Append(_domain);
}
// expiration
if (_expirationSet && _expires != DateTime.MinValue) {
s.Append("; expires=");
s.Append(HttpUtility.FormatHttpCookieDateTime(_expires));
}
// path
if (!String.IsNullOrEmpty(_path)) {
s.Append("; path=");
s.Append(_path);
}
// secure
if (_secure)
s.Append("; secure");
// httponly, Note: IE5 on the Mac doesn't support this
if (_httpOnly && SupportsHttpOnly(context)) {
s.Append("; HttpOnly");
}
// SameSite
if(_sameSite > (AppSettings.SuppressSameSiteNone ? SameSiteMode.None : (SameSiteMode)(-1) /* Unspecified */)) {
s.Append("; SameSite=");
s.Append(_sameSite);
}
// return as HttpResponseHeader
return new HttpResponseHeader(HttpWorkerRequest.HeaderSetCookie, s.ToString());
}
}
public enum HttpCookieMode {
UseUri, // cookieless=true
UseCookies, // cookieless=false
AutoDetect, // cookieless=AutoDetect; Probe if device is cookied
UseDeviceProfile // cookieless=UseDeviceProfile; Base decision on caps
}
// Due to modern browser updates, "None" is now required to be emitted in the cookie header. To prevent
// any the header from being written, Cookies should be SameSite=Unspecified. But we can't update the
// enum in a servicing patch. So...
// Unspecified == -1
public enum SameSiteMode {
None,
Lax,
Strict
}
internal class SameSiteConverter : EnumConverter
{
public SameSiteConverter() : base(typeof(SameSiteMode)) { }
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string strval = value as string;
if (strval != null && strval.Equals("Unspecified", StringComparison.InvariantCultureIgnoreCase))
return (SameSiteMode)(-1);
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (value is SameSiteMode && destinationType == typeof(string))
{
int iVal = (int)value;
if (iVal < 0)
return "Unspecified";
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
}
|