|
//------------------------------------------------------------------------------
// <copyright file="cookie.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
namespace System.Net {
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
internal enum CookieVariant {
Unknown,
Plain,
Rfc2109,
Rfc2965,
Default = Rfc2109
}
//
// Cookie class
//
// Adheres to RFC 2965
//
// Currently, only client-side cookies. The cookie classes know how to
// parse a set-cookie format string, but not a cookie format string
// (e.g. "Cookie: $Version=1; name=value; $Path=/foo; $Secure")
//
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
[Serializable]
public sealed class Cookie {
internal const int MaxSupportedVersion = 1;
internal const string CommentAttributeName = "Comment";
internal const string CommentUrlAttributeName = "CommentURL";
internal const string DiscardAttributeName = "Discard";
internal const string DomainAttributeName = "Domain";
internal const string ExpiresAttributeName = "Expires";
internal const string MaxAgeAttributeName = "Max-Age";
internal const string PathAttributeName = "Path";
internal const string PortAttributeName = "Port";
internal const string SecureAttributeName = "Secure";
internal const string VersionAttributeName = "Version";
internal const string HttpOnlyAttributeName = "HttpOnly";
internal const string SeparatorLiteral = "; ";
internal const string EqualsLiteral = "=";
internal const string QuotesLiteral = "\"";
internal const string SpecialAttributeLiteral = "$";
internal static readonly char[] PortSplitDelimiters = new char[] {' ', ',', '\"'};
internal static readonly char[] Reserved2Name = new char[] {' ', '\t', '\r', '\n', '=', ';', ',' };
internal static readonly char[] Reserved2Value = new char[] {';', ',' };
private static Comparer staticComparer = new Comparer();
// fields
string m_comment = string.Empty;
Uri m_commentUri = null;
CookieVariant m_cookieVariant = CookieVariant.Plain;
bool m_discard = false;
string m_domain = string.Empty;
bool m_domain_implicit = true;
DateTime m_expires = DateTime.MinValue;
string m_name = string.Empty;
string m_path = string.Empty;
bool m_path_implicit = true;
string m_port = string.Empty;
bool m_port_implicit = true;
int[] m_port_list = null;
bool m_secure = false;
[System.Runtime.Serialization.OptionalField]
bool m_httpOnly=false;
DateTime m_timeStamp = DateTime.Now;
string m_value = string.Empty;
int m_version = 0;
string m_domainKey = string.Empty;
internal bool IsQuotedVersion = false;
internal bool IsQuotedDomain = false;
// constructors
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public Cookie() {
}
//public Cookie(string cookie) {
// if ((cookie == null) || (cookie == String.Empty)) {
// throw new ArgumentException("cookie");
// }
// Parse(cookie.Trim());
// Validate();
//}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public Cookie(string name, string value) {
Name = name;
m_value = value;
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public Cookie(string name, string value, string path)
: this(name, value) {
Path = path;
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public Cookie(string name, string value, string path, string domain)
: this(name, value, path) {
Domain = domain;
}
// properties
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public string Comment {
get {
return m_comment;
}
set {
if (value == null) {
value = string.Empty;
}
m_comment = value;
}
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public Uri CommentUri {
get {
return m_commentUri;
}
set {
m_commentUri = value;
}
}
public bool HttpOnly{
get{
return m_httpOnly;
}
set{
m_httpOnly = value;
}
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public bool Discard {
get {
return m_discard;
}
set {
m_discard = value;
}
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public string Domain {
get {
return m_domain;
}
set {
m_domain = (value == null? String.Empty : value);
m_domain_implicit = false;
m_domainKey = string.Empty; //this will get it value when adding into the Container.
}
}
private string _Domain {
get {
return (Plain || m_domain_implicit || (m_domain.Length == 0))
? string.Empty
: (SpecialAttributeLiteral
+ DomainAttributeName
+ EqualsLiteral + (IsQuotedDomain? "\"": string.Empty)
+ m_domain + (IsQuotedDomain? "\"": string.Empty)
);
}
}
internal bool DomainImplicit {
get {
return m_domain_implicit;
}
set {
m_domain_implicit = value;
}
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public bool Expired {
get {
return (m_expires != DateTime.MinValue) && (m_expires.ToLocalTime() <= DateTime.Now);
}
set {
if (value == true) {
m_expires = DateTime.Now;
}
}
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public DateTime Expires {
get {
return m_expires;
}
set {
m_expires = value;
}
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public string Name {
get {
return m_name;
}
set {
if (ValidationHelper.IsBlankString(value) || !InternalSetName(value)) {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, "Name", value == null? "<null>": value));
}
}
}
internal bool InternalSetName(string value) {
if (ValidationHelper.IsBlankString(value) || value[0] == '$' || value.IndexOfAny(Reserved2Name) != -1) {
m_name = string.Empty;
return false;
}
m_name = value;
return true;
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public string Path {
get {
return m_path;
}
set {
m_path = (value == null? String.Empty : value);
m_path_implicit = false;
}
}
private string _Path {
get {
return (Plain || m_path_implicit || (m_path.Length == 0))
? string.Empty
: (SpecialAttributeLiteral
+ PathAttributeName
+ EqualsLiteral
+ m_path
);
}
}
internal bool Plain {
get {
return Variant == CookieVariant.Plain;
}
}
internal Cookie Clone() {
Cookie clonedCookie = new Cookie(m_name, m_value);
//
// Copy over all the properties from the original cookie
//
if (!m_port_implicit) {
clonedCookie.Port = m_port;
}
if (!m_path_implicit) {
clonedCookie.Path = m_path;
}
clonedCookie.Domain = m_domain;
//
// If the domain in the original cookie was implicit, we should preserve that property
clonedCookie.DomainImplicit = m_domain_implicit;
clonedCookie.m_timeStamp = m_timeStamp;
clonedCookie.Comment = m_comment;
clonedCookie.CommentUri = m_commentUri;
clonedCookie.HttpOnly = m_httpOnly;
clonedCookie.Discard = m_discard;
clonedCookie.Expires = m_expires;
clonedCookie.Version = m_version;
clonedCookie.Secure = m_secure;
//
// The variant is set when we set properties like port/version. So,
// we should copy over the variant from the original cookie after
// we set all other properties
clonedCookie.m_cookieVariant = m_cookieVariant;
return clonedCookie;
}
private static bool IsDomainEqualToHost(string domain, string host) {
//
// +1 in the host length is to account for the leading dot in domain
if ((host.Length + 1 == domain.Length) &&
(string.Compare(host, 0, domain, 1, host.Length, StringComparison.OrdinalIgnoreCase) == 0)) {
return true;
}
else {
return false;
}
}
//
// According to spec we must assume default values for attributes but still
// keep in mind that we must not include them into the requests.
// We also check the validiy of all attributes based on the version and variant (read RFC)
//
// To work properly this function must be called after cookie construction with
// default (response) URI AND set_default == true
//
// Afterwards, the function can be called many times with other URIs and
// set_default == false to check whether this cookie matches given uri
//
internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDomain, string localDomain, bool set_default, bool isThrow) {
string host = uri.Host;
int port = uri.Port;
string path = uri.AbsolutePath;
bool valid= true;
if (set_default) {
// Set Variant. If version is zero => reset cookie to Version0 style
if (Version == 0) {
variant = CookieVariant.Plain;
}
else if (Version == 1 && variant == CookieVariant.Unknown) {
//since we don't expose Variant to an app, set it to Default
variant = CookieVariant.Default;
}
m_cookieVariant = variant;
}
//Check the name
if (m_name == null || m_name.Length == 0 || m_name[0] == '$' || m_name.IndexOfAny(Reserved2Name) != -1) {
if (isThrow) {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, "Name", m_name == null? "<null>": m_name));
}
return false;
}
//Check the value
if (m_value == null ||
(!(m_value.Length > 2 && m_value[0] == '\"' && m_value[m_value.Length-1] == '\"') && m_value.IndexOfAny(Reserved2Value) != -1)) {
if (isThrow) {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, "Value", m_value == null? "<null>": m_value));
}
return false;
}
//Check Comment syntax
if (Comment != null && !(Comment.Length > 2 && Comment[0] == '\"' && Comment[Comment.Length-1] == '\"')
&& (Comment.IndexOfAny(Reserved2Value) != -1)) {
if (isThrow)
throw new CookieException(SR.GetString(SR.net_cookie_attribute, CommentAttributeName, Comment));
return false;
}
//Check Path syntax
if (Path != null && !(Path.Length > 2 && Path[0] == '\"' && Path[Path.Length-1] == '\"')
&& (Path.IndexOfAny(Reserved2Value) != -1)) {
if (isThrow) {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, PathAttributeName, Path));
}
return false;
}
//Check/set domain
// if domain is implicit => assume a) uri is valid, b) just set domain to uri hostname
if (set_default && m_domain_implicit == true) {
m_domain = host;
}
else {
if (!m_domain_implicit) {
// Forwarding note: If Uri.Host is of IP address form then the only supported case
// is for IMPLICIT domain property of a cookie.
// The below code (explicit cookie.Domain value) will try to parse Uri.Host IP string
// as a fqdn and reject the cookie
//aliasing since we might need the KeyValue (but not the original one)
string domain = m_domain;
//Syntax check for Domain charset plus empty string
if (!DomainCharsTest(domain)) {
if (isThrow) {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, DomainAttributeName, domain == null? "<null>": domain));
}
return false;
}
//domain must start with '.' if set explicitly
if(domain[0] != '.' ) {
if (!(variant == CookieVariant.Rfc2965 || variant == CookieVariant.Plain)) {
if (isThrow) {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, DomainAttributeName, m_domain));
}
return false;
}
domain = '.' + domain;
}
int host_dot = host.IndexOf('.');
// First quick check is for pushing a cookie into the local domain
if (isLocalDomain && (string.Compare(localDomain, domain, StringComparison.OrdinalIgnoreCase ) == 0)) {
valid = true;
}
else if (domain.IndexOf('.', 1, domain.Length-2) == -1) {
// A single label domain is valid only if the domain is exactly the same as the host specified in the URI
if (!IsDomainEqualToHost(domain, host)) {
valid = false;
}
}
else if (variant == CookieVariant.Plain) {
// We distiguish between Version0 cookie and other versions on domain issue
// According to Version0 spec a domain must be just a substring of the hostname
if (!IsDomainEqualToHost(domain, host)) {
if (host.Length <= domain.Length ||
string.Compare(host, host.Length-domain.Length, domain, 0, domain.Length, StringComparison.OrdinalIgnoreCase) != 0) {
valid = false;
}
}
}
else if (host_dot == -1 ||
domain.Length != host.Length-host_dot ||
string.Compare(host, host_dot, domain, 0, domain.Length, StringComparison.OrdinalIgnoreCase) != 0) {
//starting the first dot, the host must match the domain
// for null hosts, the host must match the domain exactly
if (!IsDomainEqualToHost(domain, host)) {
valid = false;
}
}
if (valid) {
m_domainKey = domain.ToLower(CultureInfo.InvariantCulture);
}
}
else {
// for implicitly set domain AND at the set_default==false time
// we simply got to match uri.Host against m_domain
if (string.Compare(host, m_domain, StringComparison.OrdinalIgnoreCase) != 0) {
valid = false;
}
}
if(!valid) {
if (isThrow) {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, DomainAttributeName, m_domain));
}
return false;
}
}
//Check/Set Path
if (set_default && m_path_implicit == true) {
//assuming that uri path is always valid and contains at least one '/'
switch (m_cookieVariant) {
case CookieVariant.Plain:
m_path = path;
break;
case CookieVariant.Rfc2109:
m_path = path.Substring(0, path.LastIndexOf('/')); //may be empty
break;
case CookieVariant.Rfc2965:
default: //hope future versions will have same 'Path' semantic?
m_path = path.Substring(0, path.LastIndexOf('/')+1);
break;
}
}
else {
//check current path (implicit/explicit) against given uri
if (!path.StartsWith(CookieParser.CheckQuoted(m_path))) {
if (isThrow) {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, PathAttributeName, m_path));
}
return false;
}
}
// set the default port if Port attribute was present but had no value
if (set_default && (m_port_implicit == false && m_port.Length == 0)) {
m_port_list = new int[1] {port};
}
if(m_port_implicit == false) {
// Port must match agaist the one from the uri
valid = false;
foreach (int p in m_port_list) {
if (p == port) {
valid = true;
break;
}
}
if (!valid) {
if (isThrow) {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, PortAttributeName, m_port));
}
return false;
}
}
return true;
}
//Very primitive test to make sure that the name does not have illegal characters
// As per RFC 952 (relaxed on first char could be a digit and string can have '_')
private static bool DomainCharsTest(string name) {
if (name == null || name.Length == 0) {
return false;
}
for(int i=0; i < name.Length; ++i) {
char ch = name[i];
if (!(
(ch >= '0' && ch <= '9') ||
(ch == '.' || ch == '-') ||
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch == '_')
))
return false;
}
return true;
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public string Port {
get {
return m_port;
}
set {
m_port_implicit = false;
if ((value == null || value.Length == 0)) {
//"Port" is present but has no value.
m_port = string.Empty;
}
else {
// Parse port list
if (value[0] != '\"' || value[value.Length-1] != '\"') {
throw new CookieException(SR.GetString(SR.net_cookie_attribute, PortAttributeName, value));
}
string[] ports = value.Split(PortSplitDelimiters);
List<int> portList = new List<int>();
int port;
for (int i = 0; i < ports.Length; ++i) {
// Skip spaces
if (ports[i] != string.Empty) {
if (!Int32.TryParse(ports[i], out port))
throw new CookieException(SR.GetString(SR.net_cookie_attribute, PortAttributeName, value));
// valid values for port 0 - 0xFFFF
if ((port < 0) || (port > 0xFFFF))
throw new CookieException(SR.GetString(SR.net_cookie_attribute, PortAttributeName, value));
portList.Add(port);
}
}
m_port_list = portList.ToArray();
m_port = value;
m_version = MaxSupportedVersion;
m_cookieVariant = CookieVariant.Rfc2965;
}
}
}
internal int[] PortList {
get {
//It must be null if Port Attribute was ommitted in the response
return m_port_list;
}
}
private string _Port {
get {
return m_port_implicit ? string.Empty :
(SpecialAttributeLiteral
+ PortAttributeName
+ ((m_port.Length == 0) ? string.Empty : (EqualsLiteral + m_port))
);
}
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public bool Secure {
get {
return m_secure;
}
set {
m_secure = value;
}
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public DateTime TimeStamp {
get {
return m_timeStamp;
}
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public string Value {
get {
return m_value;
}
set {
m_value = (value == null? String.Empty : value);
}
}
internal CookieVariant Variant {
get {
return m_cookieVariant;
}
set {
// only set by HttpListenerRequest::Cookies_get()
GlobalLog.Assert(value == CookieVariant.Rfc2965, "Cookie#{0}::set_Variant()|value:{1}" , ValidationHelper.HashString(this), value);
m_cookieVariant = value;
}
}
// m_domainKey member is set internally in VerifySetDefaults()
// If it is not set then verification function was not called
// and this should never happen
internal string DomainKey {
get {
return m_domain_implicit ? Domain : m_domainKey;
}
}
//public Version Version {
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public int Version {
get {
return m_version;
}
set {
if (value<0) {
throw new ArgumentOutOfRangeException("value");
}
m_version = value;
if (value>0 && m_cookieVariant<CookieVariant.Rfc2109) {
m_cookieVariant = CookieVariant.Rfc2109;
}
}
}
private string _Version {
get {
return (Version == 0) ? string.Empty :
( SpecialAttributeLiteral
+ VersionAttributeName
+ EqualsLiteral + (IsQuotedVersion? "\"": string.Empty)
+ m_version.ToString(NumberFormatInfo.InvariantInfo) + (IsQuotedVersion? "\"": string.Empty));
}
}
// methods
internal static IComparer GetComparer() {
//the class don't have any members, it's safe reuse the instance
return staticComparer;
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public override bool Equals(object comparand) {
if (!(comparand is Cookie)) {
return false;
}
Cookie other = (Cookie)comparand;
return (string.Compare(Name, other.Name, StringComparison.OrdinalIgnoreCase) == 0)
&& (string.Compare(Value, other.Value, StringComparison.Ordinal) == 0)
&& (string.Compare(Path, other.Path, StringComparison.Ordinal) == 0)
&& (string.Compare(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) == 0)
&& (Version == other.Version)
;
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public override int GetHashCode() {
//
//string hashString = Name + "=" + Value + ";" + Path + "; " + Domain + "; " + Version;
//int hash = 0;
//
//foreach (char ch in hashString) {
// hash = unchecked(hash << 1 ^ (int)ch);
//}
//return hash;
return (Name + "=" + Value + ";" + Path + "; " + Domain + "; " + Version).GetHashCode();
}
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public override string ToString() {
string domain = _Domain;
string path = _Path;
string port = _Port;
string version = _Version;
string result =
((version.Length == 0)? string.Empty : (version + SeparatorLiteral))
+ Name + EqualsLiteral + Value
+ ((path.Length == 0) ? string.Empty : (SeparatorLiteral + path))
+ ((domain.Length == 0) ? string.Empty : (SeparatorLiteral + domain))
+ ((port.Length == 0) ? string.Empty : (SeparatorLiteral + port))
;
if (result == "=") {
return string.Empty;
}
return result;
}
internal string ToServerString() {
string result = Name + EqualsLiteral + Value;
if (m_comment!=null && m_comment.Length>0) {
result += SeparatorLiteral + CommentAttributeName + EqualsLiteral + m_comment;
}
if (m_commentUri!=null) {
result += SeparatorLiteral + CommentUrlAttributeName + EqualsLiteral + QuotesLiteral + m_commentUri.ToString() + QuotesLiteral;
}
if (m_discard) {
result += SeparatorLiteral + DiscardAttributeName;
}
if (!m_domain_implicit && m_domain!=null && m_domain.Length>0) {
result += SeparatorLiteral + DomainAttributeName + EqualsLiteral + m_domain;
}
if (Expires != DateTime.MinValue) {
int seconds = (int) (Expires.ToLocalTime() - DateTime.Now).TotalSeconds;
if (seconds < 0) {
// This means that the cookie has already expired. Set Max-Age to 0
// so that the client will discard the cookie immediately
seconds = 0;
}
result += SeparatorLiteral + MaxAgeAttributeName + EqualsLiteral + seconds.ToString(NumberFormatInfo.InvariantInfo);
}
if (!m_path_implicit && m_path!=null && m_path.Length>0) {
result += SeparatorLiteral + PathAttributeName + EqualsLiteral + m_path;
}
if (!Plain && !m_port_implicit && m_port!=null && m_port.Length>0) {
// QuotesLiteral are included in m_port
result += SeparatorLiteral + PortAttributeName + EqualsLiteral + m_port;
}
if (m_version>0) {
result += SeparatorLiteral + VersionAttributeName + EqualsLiteral + m_version.ToString(NumberFormatInfo.InvariantInfo);
}
return result==EqualsLiteral ? null : result;
}
//private void Validate() {
// if ((m_name == String.Empty) && (m_value == String.Empty)) {
// throw new CookieException();
// }
// if ((m_name != String.Empty) && (m_name[0] == '$')) {
// throw new CookieException();
// }
//}
#if DEBUG
/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
internal void Dump() {
GlobalLog.Print("Cookie: " + ToString() + "->\n"
+ "\tComment = " + Comment + "\n"
+ "\tCommentUri = " + CommentUri + "\n"
+ "\tDiscard = " + Discard + "\n"
+ "\tDomain = " + Domain + "\n"
+ "\tExpired = " + Expired + "\n"
+ "\tExpires = " + Expires + "\n"
+ "\tName = " + Name + "\n"
+ "\tPath = " + Path + "\n"
+ "\tPort = " + Port + "\n"
+ "\tSecure = " + Secure + "\n"
+ "\tTimeStamp = " + TimeStamp + "\n"
+ "\tValue = " + Value + "\n"
+ "\tVariant = " + Variant + "\n"
+ "\tVersion = " + Version + "\n"
+ "\tHttpOnly = " + HttpOnly + "\n"
);
}
#endif
}
internal enum CookieToken {
// state types
Nothing,
NameValuePair, // X=Y
Attribute, // X
EndToken, // ';'
EndCookie, // ','
End, // EOLN
Equals,
// value types
Comment,
CommentUrl,
CookieName,
Discard,
Domain,
Expires,
MaxAge,
Path,
Port,
Secure,
HttpOnly,
Unknown,
Version
}
//
// CookieTokenizer
//
// Used to split a single or multi-cookie (header) string into individual
// tokens
//
internal class CookieTokenizer {
// fields
bool m_eofCookie;
int m_index;
int m_length;
string m_name;
bool m_quoted;
int m_start;
CookieToken m_token;
int m_tokenLength;
string m_tokenStream;
string m_value;
// constructors
internal CookieTokenizer(string tokenStream) {
m_length = tokenStream.Length;
m_tokenStream = tokenStream;
}
// properties
internal bool EndOfCookie {
get {
return m_eofCookie;
}
set {
m_eofCookie = value;
}
}
internal bool Eof {
get {
return m_index >= m_length;
}
}
internal string Name {
get {
return m_name;
}
set {
m_name = value;
}
}
internal bool Quoted {
get {
return m_quoted;
}
set {
m_quoted = value;
}
}
internal CookieToken Token {
get {
return m_token;
}
set {
m_token = value;
}
}
internal string Value {
get {
return m_value;
}
set {
m_value = value;
}
}
// methods
//
// Extract
//
// extract the current token
//
internal string Extract() {
string tokenString = string.Empty;
if (m_tokenLength != 0) {
tokenString = m_tokenStream.Substring(m_start, m_tokenLength);
if (!Quoted) {
tokenString = tokenString.Trim();
}
}
return tokenString;
}
//
// FindNext
//
// Find the start and length of the next token. The token is terminated
// by one of:
//
// - end-of-line
// - end-of-cookie: unquoted comma separates multiple cookies
// - end-of-token: unquoted semi-colon
// - end-of-name: unquoted equals
//
// Inputs:
// <argument> ignoreComma
// true if parsing doesn't stop at a comma. This is only true when
// we know we're parsing an original cookie that has an expires=
// attribute, because the format of the time/date used in expires
// is:
// Wdy, dd-mmm-yyyy HH:MM:SS GMT
//
// <argument> ignoreEquals
// true if parsing doesn't stop at an equals sign. The LHS of the
// first equals sign is an attribute name. The next token may
// include one or more equals signs. E.g.,
//
// SESSIONID=ID=MSNx45&q=33
//
// Outputs:
// <member> m_index
// incremented to the last position in m_tokenStream contained by
// the current token
//
// <member> m_start
// incremented to the start of the current token
//
// <member> m_tokenLength
// set to the length of the current token
//
// Assumes:
// Nothing
//
// Returns:
// type of CookieToken found:
//
// End - end of the cookie string
// EndCookie - end of current cookie in (potentially) a
// multi-cookie string
// EndToken - end of name=value pair, or end of an attribute
// Equals - end of name=
//
// Throws:
// Nothing
//
internal CookieToken FindNext(bool ignoreComma, bool ignoreEquals) {
m_tokenLength = 0;
m_start = m_index;
while ((m_index < m_length) && Char.IsWhiteSpace(m_tokenStream[m_index])) {
++m_index;
++m_start;
}
CookieToken token = CookieToken.End;
int increment = 1;
if (!Eof) {
if (m_tokenStream[m_index] == '"') {
Quoted = true;
++m_index;
bool quoteOn = false;
while (m_index < m_length) {
char currChar = m_tokenStream[m_index];
if (!quoteOn && currChar == '"')
break;
if (quoteOn)
quoteOn = false;
else if (currChar == '\\')
quoteOn = true;
++m_index;
}
if (m_index < m_length) {
++m_index;
}
m_tokenLength = m_index - m_start;
increment = 0;
// if we are here, reset ignoreComma
// In effect, we ignore everything after quoted string till next delimiter
ignoreComma = false;
}
while ((m_index < m_length)
&& (m_tokenStream[m_index] != ';')
&& (ignoreEquals || (m_tokenStream[m_index] != '='))
&& (ignoreComma || (m_tokenStream[m_index] != ','))) {
// Fixing 2 things:
// 1) ignore day of week in cookie string
// 2) revert ignoreComma once meet it, so won't miss the next cookie)
if (m_tokenStream[m_index] == ',') {
m_start = m_index+1;
m_tokenLength = -1;
ignoreComma = false;
}
++m_index;
m_tokenLength += increment;
}
if (!Eof) {
switch (m_tokenStream[m_index]) {
case ';':
token = CookieToken.EndToken;
break;
case '=':
token = CookieToken.Equals;
break;
default:
token = CookieToken.EndCookie;
break;
}
++m_index;
}
}
return token;
}
//
// Next
//
// Get the next cookie name/value or attribute
//
// Cookies come in the following formats:
//
// 1. Version0
// Set-Cookie: [<name>][=][<value>]
// [; expires=<date>]
// [; path=<path>]
// [; domain=<domain>]
// [; secure]
// Cookie: <name>=<value>
//
// Notes: <name> and/or <value> may be blank
// <date> is the RFC 822/1123 date format that
// incorporates commas, e.g.
// "Wednesday, 09-Nov-99 23:12:40 GMT"
//
// 2. RFC 2109
// Set-Cookie: 1#{
// <name>=<value>
// [; comment=<comment>]
// [; domain=<domain>]
// [; max-age=<seconds>]
// [; path=<path>]
// [; secure]
// ; Version=<version>
// }
// Cookie: $Version=<version>
// 1#{
// ; <name>=<value>
// [; path=<path>]
// [; domain=<domain>]
// }
//
// 3. RFC 2965
// Set-Cookie2: 1#{
// <name>=<value>
// [; comment=<comment>]
// [; commentURL=<comment>]
// [; discard]
// [; domain=<domain>]
// [; max-age=<seconds>]
// [; path=<path>]
// [; ports=<portlist>]
// [; secure]
// ; Version=<version>
// }
// Cookie: $Version=<version>
// 1#{
// ; <name>=<value>
// [; path=<path>]
// [; domain=<domain>]
// [; port="<port>"]
// }
// [Cookie2: $Version=<version>]
//
// Inputs:
// <argument> first
// true if this is the first name/attribute that we have looked for
// in the cookie stream
//
// Outputs:
//
// Assumes:
// Nothing
//
// Returns:
// type of CookieToken found:
//
// - Attribute
// - token was single-value. May be empty. Caller should check
// Eof or EndCookie to determine if any more action needs to
// be taken
//
// - NameValuePair
// - Name and Value are meaningful. Either may be empty
//
// Throws:
// Nothing
//
internal CookieToken Next(bool first, bool parseResponseCookies) {
Reset();
CookieToken terminator = FindNext(false, false);
if (terminator == CookieToken.EndCookie) {
EndOfCookie = true;
}
if ((terminator == CookieToken.End) || (terminator == CookieToken.EndCookie)) {
if ((Name = Extract()).Length != 0) {
Token = TokenFromName(parseResponseCookies);
return CookieToken.Attribute;
}
return terminator;
}
Name = Extract();
if (first) {
Token = CookieToken.CookieName;
}
else {
Token = TokenFromName(parseResponseCookies);
}
if (terminator == CookieToken.Equals) {
terminator = FindNext(!first && (Token == CookieToken.Expires), true);
if (terminator == CookieToken.EndCookie) {
EndOfCookie = true;
}
Value = Extract();
return CookieToken.NameValuePair;
}
else {
return CookieToken.Attribute;
}
}
//
// Reset
//
// set up this tokenizer for finding the next name/value pair or
// attribute, or end-of-[token, cookie, or line]
//
internal void Reset() {
m_eofCookie = false;
m_name = string.Empty;
m_quoted = false;
m_start = m_index;
m_token = CookieToken.Nothing;
m_tokenLength = 0;
m_value = string.Empty;
}
private struct RecognizedAttribute {
string m_name;
CookieToken m_token;
internal RecognizedAttribute(string name, CookieToken token) {
m_name = name;
m_token = token;
}
internal CookieToken Token {
get {
return m_token;
}
}
internal bool IsEqualTo(string value) {
return string.Compare(m_name, value, StringComparison.OrdinalIgnoreCase) == 0;
}
}
//
// recognized attributes in order of expected commonality
//
static RecognizedAttribute[] RecognizedAttributes = {
new RecognizedAttribute(Cookie.PathAttributeName, CookieToken.Path),
new RecognizedAttribute(Cookie.MaxAgeAttributeName, CookieToken.MaxAge),
new RecognizedAttribute(Cookie.ExpiresAttributeName, CookieToken.Expires),
new RecognizedAttribute(Cookie.VersionAttributeName, CookieToken.Version),
new RecognizedAttribute(Cookie.DomainAttributeName, CookieToken.Domain),
new RecognizedAttribute(Cookie.SecureAttributeName, CookieToken.Secure),
new RecognizedAttribute(Cookie.DiscardAttributeName, CookieToken.Discard),
new RecognizedAttribute(Cookie.PortAttributeName, CookieToken.Port),
new RecognizedAttribute(Cookie.CommentAttributeName, CookieToken.Comment),
new RecognizedAttribute(Cookie.CommentUrlAttributeName, CookieToken.CommentUrl),
new RecognizedAttribute(Cookie.HttpOnlyAttributeName, CookieToken.HttpOnly),
};
static RecognizedAttribute[] RecognizedServerAttributes = {
new RecognizedAttribute('$' + Cookie.PathAttributeName, CookieToken.Path),
new RecognizedAttribute('$' + Cookie.VersionAttributeName, CookieToken.Version),
new RecognizedAttribute('$' + Cookie.DomainAttributeName, CookieToken.Domain),
new RecognizedAttribute('$' + Cookie.PortAttributeName, CookieToken.Port),
new RecognizedAttribute('$' + Cookie.HttpOnlyAttributeName, CookieToken.HttpOnly),
};
internal CookieToken TokenFromName(bool parseResponseCookies) {
if (!parseResponseCookies) {
for (int i = 0; i < RecognizedServerAttributes.Length; ++i) {
if (RecognizedServerAttributes[i].IsEqualTo(Name)) {
return RecognizedServerAttributes[i].Token;
}
}
}
else {
for (int i = 0; i < RecognizedAttributes.Length; ++i) {
if (RecognizedAttributes[i].IsEqualTo(Name)) {
return RecognizedAttributes[i].Token;
}
}
}
return CookieToken.Unknown;
}
}
//
// CookieParser
//
// Takes a cookie header, makes cookies
//
internal class CookieParser {
// fields
CookieTokenizer m_tokenizer;
Cookie m_savedCookie;
// constructors
internal CookieParser(string cookieString) {
m_tokenizer = new CookieTokenizer(cookieString);
}
// properties
// methods
//
// Get
//
// Gets the next cookie
//
// Inputs:
// Nothing
//
// Outputs:
// Nothing
//
// Assumes:
// Nothing
//
// Returns:
// new cookie object, or null if there's no more
//
// Throws:
// Nothing
//
internal Cookie Get() {
Cookie cookie = null;
// only first ocurence of an attribute value must be counted
bool commentSet = false;
bool commentUriSet = false;
bool domainSet = false;
bool expiresSet = false;
bool pathSet = false;
bool portSet = false; //special case as it may have no value in header
bool versionSet = false;
bool secureSet = false;
bool discardSet = false;
do {
CookieToken token = m_tokenizer.Next(cookie==null, true);
if (cookie==null && (token==CookieToken.NameValuePair || token==CookieToken.Attribute)) {
cookie = new Cookie();
if (cookie.InternalSetName(m_tokenizer.Name) == false) {
//will be rejected
cookie.InternalSetName(string.Empty);
}
cookie.Value = m_tokenizer.Value;
}
else {
switch (token) {
case CookieToken.NameValuePair:
switch (m_tokenizer.Token) {
case CookieToken.Comment:
if (!commentSet) {
commentSet = true;
cookie.Comment = m_tokenizer.Value;
}
break;
case CookieToken.CommentUrl:
if (!commentUriSet) {
commentUriSet = true;
Uri parsed;
if (Uri.TryCreate(CheckQuoted(m_tokenizer.Value), UriKind.Absolute, out parsed)) {
cookie.CommentUri = parsed;
}
}
break;
case CookieToken.Domain:
if (!domainSet) {
domainSet = true;
cookie.Domain = CheckQuoted(m_tokenizer.Value);
cookie.IsQuotedDomain = m_tokenizer.Quoted;
}
break;
case CookieToken.Expires:
if (!expiresSet) {
expiresSet = true;
DateTime expires;
if (DateTime.TryParse(CheckQuoted(m_tokenizer.Value),
CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out expires)) {
cookie.Expires = expires;
}
else {
//this cookie will be rejected
cookie.InternalSetName(string.Empty);
}
}
break;
case CookieToken.MaxAge:
if (!expiresSet) {
expiresSet = true;
int parsed;
if (int.TryParse(CheckQuoted(m_tokenizer.Value), out parsed)) {
cookie.Expires = DateTime.Now.AddSeconds((double)parsed);
}
else {
//this cookie will be rejected
cookie.InternalSetName(string.Empty);
}
}
break;
case CookieToken.Path:
if (!pathSet) {
pathSet = true;
cookie.Path = m_tokenizer.Value;
}
break;
case CookieToken.Port:
if (!portSet) {
portSet = true;
try {
cookie.Port = m_tokenizer.Value;
}
catch {
//this cookie will be rejected
cookie.InternalSetName(string.Empty);
}
}
break;
case CookieToken.Version:
if (!versionSet) {
versionSet = true;
int parsed;
if (int.TryParse(CheckQuoted(m_tokenizer.Value), out parsed)) {
cookie.Version = parsed;
cookie.IsQuotedVersion = m_tokenizer.Quoted;
}
else {
//this cookie will be rejected
cookie.InternalSetName(string.Empty);
}
}
break;
}
break;
case CookieToken.Attribute:
switch (m_tokenizer.Token) {
case CookieToken.Discard:
if (!discardSet) {
discardSet = true;
cookie.Discard = true;
}
break;
case CookieToken.Secure:
if (!secureSet) {
secureSet = true;
cookie.Secure = true;
}
break;
case CookieToken.HttpOnly:
cookie.HttpOnly = true;
break;
case CookieToken.Port:
if (!portSet) {
portSet = true;
cookie.Port = string.Empty;
}
break;
}
break;
}
}
} while (!m_tokenizer.Eof && !m_tokenizer.EndOfCookie);
return cookie;
}
// twin parsing method, different enough that it's better to split it into
// a different method
internal Cookie GetServer() {
Cookie cookie = m_savedCookie;
m_savedCookie = null;
// only first ocurence of an attribute value must be counted
bool domainSet = false;
bool pathSet = false;
bool portSet = false; //special case as it may have no value in header
do {
bool first = cookie==null || cookie.Name==null || cookie.Name.Length==0;
CookieToken token = m_tokenizer.Next(first, false);
if (first && (token==CookieToken.NameValuePair || token==CookieToken.Attribute)) {
if (cookie==null) {
cookie = new Cookie();
}
if (cookie.InternalSetName(m_tokenizer.Name) == false) {
//will be rejected
cookie.InternalSetName(string.Empty);
}
cookie.Value = m_tokenizer.Value;
}
else {
switch (token) {
case CookieToken.NameValuePair:
switch (m_tokenizer.Token) {
case CookieToken.Domain:
if (!domainSet) {
domainSet = true;
cookie.Domain = CheckQuoted(m_tokenizer.Value);
cookie.IsQuotedDomain = m_tokenizer.Quoted;
}
break;
case CookieToken.Path:
if (!pathSet) {
pathSet = true;
cookie.Path = m_tokenizer.Value;
}
break;
case CookieToken.Port:
if (!portSet) {
portSet = true;
try {
cookie.Port = m_tokenizer.Value;
}
catch (CookieException) {
//this cookie will be rejected
cookie.InternalSetName(string.Empty);
}
}
break;
case CookieToken.Version:
// this is a new cookie, this token is for the next cookie.
m_savedCookie = new Cookie();
int parsed;
if (int.TryParse(m_tokenizer.Value, out parsed)) {
m_savedCookie.Version = parsed;
}
return cookie;
case CookieToken.Unknown:
// this is a new cookie, the token is for the next cookie.
m_savedCookie = new Cookie();
if (m_savedCookie.InternalSetName(m_tokenizer.Name) == false) {
//will be rejected
m_savedCookie.InternalSetName(string.Empty);
}
m_savedCookie.Value = m_tokenizer.Value;
return cookie;
}
break;
case CookieToken.Attribute:
switch (m_tokenizer.Token) {
case CookieToken.Port:
if (!portSet) {
portSet = true;
cookie.Port = string.Empty;
}
break;
}
break;
}
}
} while (!m_tokenizer.Eof && !m_tokenizer.EndOfCookie);
return cookie;
}
internal static string CheckQuoted(string value) {
if (value.Length < 2 || value[0] != '\"' || value[value.Length-1] != '\"')
return value;
return value.Length == 2? string.Empty: value.Substring(1, value.Length-2);
}
internal bool EndofHeader()
{
return m_tokenizer.Eof;
}
}
internal class Comparer: IComparer {
int IComparer.Compare(object ol, object or) {
Cookie left = (Cookie) ol;
Cookie right = (Cookie) or;
int result;
if ((result = string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase)) != 0) {
return result;
}
if ((result = string.Compare(left.Domain, right.Domain, StringComparison.OrdinalIgnoreCase)) != 0) {
return result;
}
//
//NB: The only path is case sensitive as per spec.
// However, on Win Platform that may break some lazy applications.
//
if ((result = string.Compare(left.Path, right.Path, StringComparison.Ordinal)) != 0) {
return result;
}
// They are equal here even if variants are still different.
return 0;
}
}
}
|