aspnetcore/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs

524 lines
18 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Text;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
// http://tools.ietf.org/html/rfc6265
public class SetCookieHeaderValue
{
private const string ExpiresToken = "expires";
private const string MaxAgeToken = "max-age";
private const string DomainToken = "domain";
private const string PathToken = "path";
private const string SecureToken = "secure";
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
private const string SameSiteToken = "samesite";
private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower();
private const string HttpOnlyToken = "httponly";
private const string SeparatorToken = "; ";
private const string EqualsToken = "=";
private const string DefaultPath = "/"; // TODO: Used?
private static readonly HttpHeaderParser<SetCookieHeaderValue> SingleValueParser
= new GenericHeaderParser<SetCookieHeaderValue>(false, GetSetCookieLength);
private static readonly HttpHeaderParser<SetCookieHeaderValue> MultipleValueParser
= new GenericHeaderParser<SetCookieHeaderValue>(true, GetSetCookieLength);
private string _name;
private string _value;
private SetCookieHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
public SetCookieHeaderValue(string name)
: this(name, string.Empty)
{
}
public SetCookieHeaderValue(string name, string value)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
Name = name;
Value = value;
}
public string Name
{
get { return _name; }
set
{
CookieHeaderValue.CheckNameFormat(value, nameof(value));
_name = value;
}
}
public string Value
{
get { return _value; }
set
{
CookieHeaderValue.CheckValueFormat(value, nameof(value));
_value = value;
}
}
public DateTimeOffset? Expires { get; set; }
public TimeSpan? MaxAge { get; set; }
public string Domain { get; set; }
// TODO: PathString?
public string Path { get; set; }
public bool Secure { get; set; }
public SameSiteMode SameSite { get; set; }
public bool HttpOnly { get; set; }
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
public override string ToString()
{
var length = _name.Length + EqualsToken.Length + _value.Length;
string expires = null;
string maxAge = null;
string sameSite = null;
if (Expires.HasValue)
{
expires = HeaderUtilities.FormatDate(Expires.Value);
length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + expires.Length;
}
if (MaxAge.HasValue)
{
maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds);
length += SeparatorToken.Length + MaxAgeToken.Length + EqualsToken.Length + maxAge.Length;
}
if (Domain != null)
{
length += SeparatorToken.Length + DomainToken.Length + EqualsToken.Length + Domain.Length;
}
if (Path != null)
{
length += SeparatorToken.Length + PathToken.Length + EqualsToken.Length + Path.Length;
}
if (Secure)
{
length += SeparatorToken.Length + SecureToken.Length;
}
if (SameSite != SameSiteMode.None)
{
sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
}
if (HttpOnly)
{
length += SeparatorToken.Length + HttpOnlyToken.Length;
}
var sb = new InplaceStringBuilder(length);
sb.Append(_name);
sb.Append(EqualsToken);
sb.Append(_value);
if (expires != null)
{
AppendSegment(ref sb, ExpiresToken, expires);
}
if (maxAge != null)
{
AppendSegment(ref sb, MaxAgeToken, maxAge);
}
if (Domain != null)
{
AppendSegment(ref sb, DomainToken, Domain);
}
if (Path != null)
{
AppendSegment(ref sb, PathToken, Path);
}
if (Secure)
{
AppendSegment(ref sb, SecureToken, null);
}
if (SameSite != SameSiteMode.None)
{
AppendSegment(ref sb, SameSiteToken, sameSite);
}
if (HttpOnly)
{
AppendSegment(ref sb, HttpOnlyToken, null);
}
return sb.ToString();
}
private static void AppendSegment(ref InplaceStringBuilder builder, string name, string value)
{
builder.Append(SeparatorToken);
builder.Append(name);
if (value != null)
{
builder.Append(EqualsToken);
builder.Append(value);
}
}
/// <summary>
/// Append string representation of this <see cref="SetCookieHeaderValue"/> to given
/// <paramref name="builder"/>.
/// </summary>
/// <param name="builder">
/// The <see cref="StringBuilder"/> to receive the string representation of this
/// <see cref="SetCookieHeaderValue"/>.
/// </param>
public void AppendToStringBuilder(StringBuilder builder)
{
builder.Append(_name);
builder.Append("=");
builder.Append(_value);
if (Expires.HasValue)
{
AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value));
}
if (MaxAge.HasValue)
{
AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds));
}
if (Domain != null)
{
AppendSegment(builder, DomainToken, Domain);
}
if (Path != null)
{
AppendSegment(builder, PathToken, Path);
}
if (Secure)
{
AppendSegment(builder, SecureToken, null);
}
if (SameSite != SameSiteMode.None)
{
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
}
if (HttpOnly)
{
AppendSegment(builder, HttpOnlyToken, null);
}
}
private static void AppendSegment(StringBuilder builder, string name, string value)
{
builder.Append("; ");
builder.Append(name);
if (value != null)
{
builder.Append("=");
builder.Append(value);
}
}
public static SetCookieHeaderValue Parse(string input)
{
var index = 0;
return SingleValueParser.ParseValue(input, ref index);
}
public static bool TryParse(string input, out SetCookieHeaderValue parsedValue)
{
var index = 0;
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
}
public static IList<SetCookieHeaderValue> ParseList(IList<string> inputs)
{
return MultipleValueParser.ParseValues(inputs);
}
public static IList<SetCookieHeaderValue> ParseStrictList(IList<string> inputs)
{
return MultipleValueParser.ParseStrictValues(inputs);
}
public static bool TryParseList(IList<string> inputs, out IList<SetCookieHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
}
public static bool TryParseStrictList(IList<string> inputs, out IList<SetCookieHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
}
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
private static int GetSetCookieLength(string input, int startIndex, out SetCookieHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
var offset = startIndex;
parsedValue = null;
if (string.IsNullOrEmpty(input) || (offset >= input.Length))
{
return 0;
}
var result = new SetCookieHeaderValue();
// The caller should have already consumed any leading whitespace, commas, etc..
// Name=value;
// Name
var itemLength = HttpRuleParser.GetTokenLength(input, offset);
if (itemLength == 0)
{
return 0;
}
result._name = input.Substring(offset, itemLength);
offset += itemLength;
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
// value or "quoted value"
// The value may be empty
result._value = CookieHeaderValue.GetCookieValue(input, ref offset);
// *(';' SP cookie-av)
while (offset < input.Length)
{
if (input[offset] == ',')
{
// Divider between headers
break;
}
if (input[offset] != ';')
{
// Expecting a ';' between parameters
return 0;
}
offset++;
offset += HttpRuleParser.GetWhitespaceLength(input, offset);
// cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / samesite-av / httponly-av / extension-av
itemLength = HttpRuleParser.GetTokenLength(input, offset);
if (itemLength == 0)
{
// Trailing ';' or leading into garbage. Let the next parser fail.
break;
}
var token = input.Substring(offset, itemLength);
offset += itemLength;
// expires-av = "Expires=" sane-cookie-date
if (string.Equals(token, ExpiresToken, StringComparison.OrdinalIgnoreCase))
{
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
var dateString = ReadToSemicolonOrEnd(input, ref offset);
DateTimeOffset expirationDate;
if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate))
{
// Invalid expiration date, abort
return 0;
}
result.Expires = expirationDate;
}
// max-age-av = "Max-Age=" non-zero-digit *DIGIT
else if (string.Equals(token, MaxAgeToken, StringComparison.OrdinalIgnoreCase))
{
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
itemLength = HttpRuleParser.GetNumberLength(input, offset, allowDecimal: false);
if (itemLength == 0)
{
return 0;
}
var numberString = input.Substring(offset, itemLength);
long maxAge;
if (!HeaderUtilities.TryParseNonNegativeInt64(numberString, out maxAge))
{
// Invalid expiration date, abort
return 0;
}
result.MaxAge = TimeSpan.FromSeconds(maxAge);
offset += itemLength;
}
// domain-av = "Domain=" domain-value
// domain-value = <subdomain> ; defined in [RFC1034], Section 3.5, as enhanced by [RFC1123], Section 2.1
else if (string.Equals(token, DomainToken, StringComparison.OrdinalIgnoreCase))
{
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
// We don't do any detailed validation on the domain.
result.Domain = ReadToSemicolonOrEnd(input, ref offset);
}
// path-av = "Path=" path-value
// path-value = <any CHAR except CTLs or ";">
else if (string.Equals(token, PathToken, StringComparison.OrdinalIgnoreCase))
{
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
// We don't do any detailed validation on the path.
result.Path = ReadToSemicolonOrEnd(input, ref offset);
}
// secure-av = "Secure"
else if (string.Equals(token, SecureToken, StringComparison.OrdinalIgnoreCase))
{
result.Secure = true;
}
// samesite-av = "SameSite" / "SameSite=" samesite-value
// samesite-value = "Strict" / "Lax"
else if (string.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
{
if (!ReadEqualsSign(input, ref offset))
{
result.SameSite = SameSiteMode.Strict;
}
else
{
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
if (string.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
{
result.SameSite = SameSiteMode.Lax;
}
else
{
result.SameSite = SameSiteMode.Strict;
}
}
}
// httponly-av = "HttpOnly"
else if (string.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase))
{
result.HttpOnly = true;
}
// extension-av = <any CHAR except CTLs or ";">
else
{
// TODO: skip it? Store it in a list?
}
}
parsedValue = result;
return offset - startIndex;
}
private static bool ReadEqualsSign(string input, ref int offset)
{
// = (no spaces)
if (offset >= input.Length || input[offset] != '=')
{
return false;
}
offset++;
return true;
}
private static string ReadToSemicolonOrEnd(string input, ref int offset)
{
var end = input.IndexOf(';', offset);
if (end < 0)
{
// Remainder of the string
end = input.Length;
}
var itemLength = end - offset;
var result = input.Substring(offset, itemLength);
offset += itemLength;
return result;
}
public override bool Equals(object obj)
{
var other = obj as SetCookieHeaderValue;
if (other == null)
{
return false;
}
return string.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase)
&& string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase)
&& Expires.Equals(other.Expires)
&& MaxAge.Equals(other.MaxAge)
&& string.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase)
&& string.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)
&& Secure == other.Secure
&& SameSite == other.SameSite
&& HttpOnly == other.HttpOnly;
}
public override int GetHashCode()
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(_name)
^ StringComparer.OrdinalIgnoreCase.GetHashCode(_value)
^ (Expires.HasValue ? Expires.GetHashCode() : 0)
^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0)
^ (Domain != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0)
^ (Path != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0)
^ Secure.GetHashCode()
^ SameSite.GetHashCode()
^ HttpOnly.GetHashCode();
}
}
}