// <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; } }