blob: 292757bf7652f607e034720299e002cb5123834f [file] [log] [blame]
// <copyright file="Cookie.cs" company="Selenium Committers">
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// </copyright>
using OpenQA.Selenium.Internal;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
namespace OpenQA.Selenium;
/// <summary>
/// Represents a cookie in the browser.
/// </summary>
[Serializable]
public class Cookie
{
private readonly string cookieName;
private readonly string cookieValue;
private readonly string? cookiePath;
private readonly string? cookieDomain;
private readonly string? sameSite;
private readonly bool isHttpOnly;
private readonly bool secure;
private readonly DateTime? cookieExpiry;
private readonly HashSet<string?> sameSiteValues = new HashSet<string?>()
{
"Strict",
"Lax",
"None"
};
/// <summary>
/// Initializes a new instance of the <see cref="Cookie"/> class with a specific name and value.
/// </summary>
/// <param name="name">The name of the cookie.</param>
/// <param name="value">The value of the cookie.</param>
/// <exception cref="ArgumentException">If the name is <see langword="null"/> or an empty string,
/// or if it contains a semi-colon.</exception>
/// <exception cref="ArgumentNullException">If the value is <see langword="null"/>.</exception>
public Cookie(string name, string value)
: this(name, value, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Cookie"/> class with a specific name,
/// value, and path.
/// </summary>
/// <param name="name">The name of the cookie.</param>
/// <param name="value">The value of the cookie.</param>
/// <param name="path">The path of the cookie.</param>
/// <exception cref="ArgumentException">If the name is <see langword="null"/> or an empty string,
/// or if it contains a semi-colon.</exception>
/// <exception cref="ArgumentNullException">If the value is <see langword="null"/>.</exception>
public Cookie(string name, string value, string? path)
: this(name, value, path, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Cookie"/> class with a specific name,
/// value, path and expiration date.
/// </summary>
/// <param name="name">The name of the cookie.</param>
/// <param name="value">The value of the cookie.</param>
/// <param name="path">The path of the cookie.</param>
/// <param name="expiry">The expiration date of the cookie.</param>
/// <exception cref="ArgumentException">If the name is <see langword="null"/> or an empty string,
/// or if it contains a semi-colon.</exception>
/// <exception cref="ArgumentNullException">If the value is <see langword="null"/>.</exception>
public Cookie(string name, string value, string? path, DateTime? expiry)
: this(name, value, null, path, expiry)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Cookie"/> class with a specific name,
/// value, domain, path and expiration date.
/// </summary>
/// <param name="name">The name of the cookie.</param>
/// <param name="value">The value of the cookie.</param>
/// <param name="domain">The domain of the cookie.</param>
/// <param name="path">The path of the cookie.</param>
/// <param name="expiry">The expiration date of the cookie.</param>
/// <exception cref="ArgumentException">If the name is <see langword="null"/> or an empty string,
/// or if it contains a semi-colon.</exception>
/// <exception cref="ArgumentNullException">If the value is <see langword="null"/>.</exception>
public Cookie(string name, string value, string? domain, string? path, DateTime? expiry)
: this(name, value, domain, path, expiry, false, false, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ReturnedCookie"/> class with a specific name,
/// value, domain, path and expiration date.
/// </summary>
/// <param name="name">The name of the cookie.</param>
/// <param name="value">The value of the cookie.</param>
/// <param name="domain">The domain of the cookie.</param>
/// <param name="path">The path of the cookie.</param>
/// <param name="expiry">The expiration date of the cookie.</param>
/// <param name="secure"><see langword="true"/> if the cookie is secure; otherwise <see langword="false"/></param>
/// <param name="isHttpOnly"><see langword="true"/> if the cookie is an HTTP-only cookie; otherwise <see langword="false"/></param>
/// <param name="sameSite">The SameSite value of cookie.</param>
/// <exception cref="ArgumentException">If the name and value are both an empty string,
/// if the name contains a semi-colon, or if same site value is not valid.</exception>
/// <exception cref="ArgumentNullException">If the name, value or currentUrl is <see langword="null"/>.</exception>
public Cookie(string name, string value, string? domain, string? path, DateTime? expiry, bool secure, bool isHttpOnly, string? sameSite)
{
if (name == null)
{
throw new ArgumentNullException(nameof(value), "Cookie name cannot be null");
}
if (value == null)
{
throw new ArgumentNullException(nameof(value), "Cookie value cannot be null");
}
if (name == string.Empty && value == string.Empty)
{
throw new ArgumentException("Cookie name and value cannot both be empty string");
}
if (name.Contains(';'))
{
throw new ArgumentException("Cookie names cannot contain a ';': " + name, nameof(name));
}
this.cookieName = name;
this.cookieValue = value;
if (!string.IsNullOrEmpty(path))
{
this.cookiePath = path;
}
this.cookieDomain = StripPort(domain);
if (expiry != null)
{
this.cookieExpiry = expiry;
}
this.isHttpOnly = isHttpOnly;
this.secure = secure;
if (!string.IsNullOrEmpty(sameSite))
{
if (!sameSiteValues.Contains(sameSite))
{
throw new ArgumentException("Invalid sameSite cookie value. It should either \"Lax\", \"Strict\" or \"None\" ", nameof(sameSite));
}
this.sameSite = sameSite;
}
}
/// <summary>
/// Gets the name of the cookie.
/// </summary>
[JsonPropertyName("name")]
public string Name => this.cookieName;
/// <summary>
/// Gets the value of the cookie.
/// </summary>
[JsonPropertyName("value")]
public string Value => this.cookieValue;
/// <summary>
/// Gets the domain of the cookie.
/// </summary>
[JsonPropertyName("domain")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Domain => this.cookieDomain;
/// <summary>
/// Gets the path of the cookie.
/// </summary>
[JsonPropertyName("path")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public virtual string? Path => this.cookiePath;
/// <summary>
/// Gets a value indicating whether the cookie is secure.
/// </summary>
[JsonPropertyName("secure")]
public virtual bool Secure => this.secure;
/// <summary>
/// Gets a value indicating whether the cookie is an HTTP-only cookie.
/// </summary>
[JsonPropertyName("httpOnly")]
public virtual bool IsHttpOnly => this.isHttpOnly;
/// <summary>
/// Gets the SameSite setting for the cookie.
/// </summary>
[JsonPropertyName("sameSite")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public virtual string? SameSite => this.sameSite;
/// <summary>
/// Gets the expiration date of the cookie.
/// </summary>
[JsonIgnore]
public DateTime? Expiry => this.cookieExpiry;
/// <summary>
/// Gets the cookie expiration date in seconds from the defined zero date (01 January 1970 00:00:00 UTC).
/// </summary>
/// <remarks>This property only exists so that the JSON serializer can serialize a
/// cookie without resorting to a custom converter.</remarks>
[JsonPropertyName("expiry")]
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
internal long? ExpirySeconds
{
get
{
if (this.cookieExpiry == null)
{
return null;
}
DateTime zeroDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
TimeSpan span = this.cookieExpiry.Value.ToUniversalTime().Subtract(zeroDate);
long totalSeconds = Convert.ToInt64(span.TotalSeconds);
return totalSeconds;
}
}
/// <summary>
/// Converts a Dictionary to a Cookie.
/// </summary>
/// <param name="rawCookie">The Dictionary object containing the cookie parameters.</param>
/// <returns>A <see cref="Cookie"/> object with the proper parameters set.</returns>
public static Cookie FromDictionary(Dictionary<string, object?> rawCookie)
{
if (rawCookie == null)
{
throw new ArgumentNullException(nameof(rawCookie));
}
string name = rawCookie["name"]!.ToString()!;
string value = string.Empty;
if (rawCookie.TryGetValue("value", out object? valueObj))
{
value = valueObj!.ToString()!;
}
string path = "/";
if (rawCookie.TryGetValue("path", out object? pathObj) && pathObj != null)
{
path = pathObj.ToString()!;
}
string domain = string.Empty;
if (rawCookie.TryGetValue("domain", out object? domainObj) && domainObj != null)
{
domain = domainObj.ToString()!;
}
DateTime? expires = null;
if (rawCookie.TryGetValue("expiry", out object? expiryObj) && expiryObj != null)
{
expires = ConvertExpirationTime(expiryObj.ToString()!);
}
bool secure = false;
if (rawCookie.TryGetValue("secure", out object? secureObj) && secureObj != null)
{
secure = bool.Parse(secureObj.ToString()!);
}
bool isHttpOnly = false;
if (rawCookie.TryGetValue("httpOnly", out object? httpOnlyObj) && httpOnlyObj != null)
{
isHttpOnly = bool.Parse(httpOnlyObj.ToString()!);
}
string? sameSite = null;
if (rawCookie.TryGetValue("sameSite", out object? sameSiteObj))
{
sameSite = sameSiteObj?.ToString();
}
return new ReturnedCookie(name, value, domain, path, expires, secure, isHttpOnly, sameSite);
}
/// <summary>
/// Creates and returns a string representation of the cookie.
/// </summary>
/// <returns>A string representation of the cookie.</returns>
public override string ToString()
{
return this.cookieName + "=" + this.cookieValue
+ (this.cookieExpiry == null ? string.Empty : "; expires=" + this.cookieExpiry.Value.ToUniversalTime().ToString("ddd MM dd yyyy hh:mm:ss UTC", CultureInfo.InvariantCulture))
+ (string.IsNullOrEmpty(this.cookiePath) ? string.Empty : "; path=" + this.cookiePath)
+ (string.IsNullOrEmpty(this.cookieDomain) ? string.Empty : "; domain=" + this.cookieDomain)
+ "; isHttpOnly= " + this.isHttpOnly + "; secure= " + this.secure + (string.IsNullOrEmpty(this.sameSite) ? string.Empty : "; sameSite=" + this.sameSite);
}
/// <summary>
/// Determines whether the specified <see cref="object">Object</see> is equal
/// to the current <see cref="object">Object</see>.
/// </summary>
/// <param name="obj">The <see cref="object">Object</see> to compare with the
/// current <see cref="object">Object</see>.</param>
/// <returns><see langword="true"/> if the specified <see cref="object">Object</see>
/// is equal to the current <see cref="object">Object</see>; otherwise,
/// <see langword="false"/>.</returns>
public override bool Equals(object? obj)
{
// Two cookies are equal if the name and value match
if (this == obj)
{
return true;
}
if (obj is not Cookie cookie)
{
return false;
}
if (!this.cookieName.Equals(cookie.cookieName))
{
return false;
}
return string.Equals(this.cookieValue, cookie.cookieValue);
}
/// <summary>
/// Serves as a hash function for a particular type.
/// </summary>
/// <returns>A hash code for the current <see cref="object">Object</see>.</returns>
public override int GetHashCode()
{
return this.cookieName.GetHashCode();
}
private static string? StripPort(string? domain)
{
return string.IsNullOrEmpty(domain) ? null : domain!.Split(':')[0];
}
private static DateTime? ConvertExpirationTime(string expirationTime)
{
DateTime? expires = null;
if (double.TryParse(expirationTime, NumberStyles.Number, CultureInfo.InvariantCulture, out double seconds))
{
try
{
expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(seconds).ToLocalTime();
}
catch (ArgumentOutOfRangeException)
{
expires = DateTime.MaxValue.ToLocalTime();
}
}
return expires;
}
}