// // System.Net.CookieContainer // // Authors: // Lawrence Pit (loz@cable.a2000.nl) // Gonzalo Paniagua Javier (gonzalo@ximian.com) // Sebastien Pouliot // // (c) 2003 Ximian, Inc. (http://www.ximian.com) // (c) Copyright 2004 Ximian, Inc. (http://www.ximian.com) // Copyright (C) 2009 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections; using System.Globalization; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; namespace System.Net { [Serializable] #if MOONLIGHT #if INSIDE_SYSTEM internal sealed class CookieContainer { #else public sealed class CookieContainer { #endif #else public class CookieContainer { #endif public const int DefaultCookieLengthLimit = 4096; public const int DefaultCookieLimit = 300; public const int DefaultPerDomainCookieLimit = 20; int capacity = DefaultCookieLimit; int perDomainCapacity = DefaultPerDomainCookieLimit; int maxCookieSize = DefaultCookieLengthLimit; CookieCollection cookies; // ctors public CookieContainer () { } public CookieContainer (int capacity) { if (capacity <= 0) throw new ArgumentException ("Must be greater than zero", "Capacity"); this.capacity = capacity; } public CookieContainer (int capacity, int perDomainCapacity, int maxCookieSize) : this (capacity) { if (perDomainCapacity != Int32.MaxValue && (perDomainCapacity <= 0 || perDomainCapacity > capacity)) throw new ArgumentOutOfRangeException ("perDomainCapacity", string.Format ("PerDomainCapacity must be " + "greater than {0} and less than {1}.", 0, capacity)); if (maxCookieSize <= 0) throw new ArgumentException ("Must be greater than zero", "MaxCookieSize"); this.perDomainCapacity = perDomainCapacity; this.maxCookieSize = maxCookieSize; } // properties public int Count { get { return (cookies == null) ? 0 : cookies.Count; } } public int Capacity { get { return capacity; } set { if (value < 0 || (value < perDomainCapacity && perDomainCapacity != Int32.MaxValue)) throw new ArgumentOutOfRangeException ("value", string.Format ("Capacity must be greater " + "than {0} and less than {1}.", 0, perDomainCapacity)); capacity = value; } } public int MaxCookieSize { get { return maxCookieSize; } set { if (value <= 0) throw new ArgumentOutOfRangeException ("value"); maxCookieSize = value; } } public int PerDomainCapacity { get { return perDomainCapacity; } set { if (value != Int32.MaxValue && (value <= 0 || value > capacity)) throw new ArgumentOutOfRangeException ("value"); perDomainCapacity = value; } } public void Add (Cookie cookie) { if (cookie == null) throw new ArgumentNullException ("cookie"); if (cookie.Domain.Length == 0) throw new ArgumentException ("Cookie domain not set.", "cookie.Domain"); if (cookie.Value.Length > maxCookieSize) throw new CookieException ("value is larger than MaxCookieSize."); // .NET's Add (Cookie) is fundamentally broken and does not copy properties // like Secure, HttpOnly and Expires so we clone the parts that .NET // does keep before calling AddCookie Cookie c = new Cookie (cookie.Name, cookie.Value); c.Path = (cookie.Path.Length == 0) ? "/" : cookie.Path; c.Domain = cookie.Domain; c.ExactDomain = cookie.ExactDomain; c.Version = cookie.Version; AddCookie (c); } void AddCookie (Cookie cookie) { if (cookies == null) cookies = new CookieCollection (); if (cookies.Count >= capacity) RemoveOldest (null); // try to avoid counting per-domain if (cookies.Count >= perDomainCapacity) { if (CountDomain (cookie.Domain) >= perDomainCapacity) RemoveOldest (cookie.Domain); } // clone the important parts of the cookie Cookie c = new Cookie (cookie.Name, cookie.Value); c.Path = (cookie.Path.Length == 0) ? "/" : cookie.Path; c.Domain = cookie.Domain; c.ExactDomain = cookie.ExactDomain; c.Version = cookie.Version; c.Expires = cookie.Expires; c.CommentUri = cookie.CommentUri; c.Comment = cookie.Comment; c.Discard = cookie.Discard; c.HttpOnly = cookie.HttpOnly; c.Secure = cookie.Secure; cookies.Add (c); CheckExpiration (); } int CountDomain (string domain) { int count = 0; foreach (Cookie c in cookies) { if (CheckDomain (domain, c.Domain, true)) count++; } return count; } void RemoveOldest (string domain) { int n = 0; DateTime oldest = DateTime.MaxValue; for (int i = 0; i < cookies.Count; i++) { Cookie c = cookies [i]; if ((c.TimeStamp < oldest) && ((domain == null) || (domain == c.Domain))) { oldest = c.TimeStamp; n = i; } } cookies.List.RemoveAt (n); } // Only needs to be called from AddCookie (Cookie) and GetCookies (Uri) void CheckExpiration () { if (cookies == null) return; for (int i = cookies.Count - 1; i >= 0; i--) { Cookie cookie = cookies [i]; if (cookie.Expired) cookies.List.RemoveAt (i); } } public void Add (CookieCollection cookies) { if (cookies == null) throw new ArgumentNullException ("cookies"); foreach (Cookie cookie in cookies) Add (cookie); } void Cook (Uri uri, Cookie cookie) { if (String.IsNullOrEmpty (cookie.Name)) throw new CookieException ("Invalid cookie: name"); if (cookie.Value == null) throw new CookieException ("Invalid cookie: value"); if (uri != null && cookie.Domain.Length == 0) cookie.Domain = uri.Host; if (cookie.Version == 0 && String.IsNullOrEmpty (cookie.Path)) { if (uri != null) { cookie.Path = uri.AbsolutePath; } else { cookie.Path = "/"; } } if (cookie.Port.Length == 0 && uri != null && !uri.IsDefaultPort) { cookie.Port = "\"" + uri.Port.ToString () + "\""; } } public void Add (Uri uri, Cookie cookie) { if (uri == null) throw new ArgumentNullException ("uri"); if (cookie == null) throw new ArgumentNullException ("cookie"); if (!cookie.Expired) { Cook (uri, cookie); AddCookie (cookie); } } public void Add (Uri uri, CookieCollection cookies) { if (uri == null) throw new ArgumentNullException ("uri"); if (cookies == null) throw new ArgumentNullException ("cookies"); foreach (Cookie cookie in cookies) { if (!cookie.Expired) { Cook (uri, cookie); AddCookie (cookie); } } } public string GetCookieHeader (Uri uri) { if (uri == null) throw new ArgumentNullException ("uri"); CookieCollection coll = GetCookies (uri); if (coll.Count == 0) return ""; StringBuilder result = new StringBuilder (); foreach (Cookie cookie in coll) { // don't include the domain since it can be infered from the URI // include empty path as '/' result.Append (cookie.ToString (uri)); result.Append ("; "); } if (result.Length > 0) result.Length -= 2; // remove trailing semicolon and space return result.ToString (); } static bool CheckDomain (string domain, string host, bool exact) { if (domain.Length == 0) return false; if (exact) return (String.Compare (host, domain, StringComparison.InvariantCultureIgnoreCase) == 0); // check for allowed sub-domains - without string allocations if (!host.EndsWith (domain, StringComparison.InvariantCultureIgnoreCase)) return false; // mono.com -> www.mono.com is OK but supermono.com NOT OK if (domain [0] == '.') return true; int p = host.Length - domain.Length - 1; if (p < 0) return false; return (host [p] == '.'); } public CookieCollection GetCookies (Uri uri) { if (uri == null) throw new ArgumentNullException ("uri"); CheckExpiration (); CookieCollection coll = new CookieCollection (); if (cookies == null) return coll; foreach (Cookie cookie in cookies) { string domain = cookie.Domain; if (!CheckDomain (domain, uri.Host, cookie.ExactDomain)) continue; if (cookie.Port.Length > 0 && cookie.Ports != null && uri.Port != -1) { if (Array.IndexOf (cookie.Ports, uri.Port) == -1) continue; } string path = cookie.Path; string uripath = uri.AbsolutePath; if (path != "" && path != "/") { if (uripath != path) { if (!uripath.StartsWith (path)) continue; if (path [path.Length - 1] != '/' && uripath.Length > path.Length && uripath [path.Length] != '/') continue; } } if (cookie.Secure && uri.Scheme != "https") continue; coll.Add (cookie); } coll.Sort (); return coll; } public void SetCookies (Uri uri, string cookieHeader) { if (uri == null) throw new ArgumentNullException ("uri"); if (cookieHeader == null) throw new ArgumentNullException ("cookieHeader"); if (cookieHeader.Length == 0) return; // Cookies must be separated by ',' (like documented on MSDN) // but expires uses DAY, DD-MMM-YYYY HH:MM:SS GMT, so simple ',' search is wrong. // See http://msdn.microsoft.com/en-us/library/aa384321%28VS.85%29.aspx string [] jar = cookieHeader.Split (','); string tmpCookie; for (int i = 0; i < jar.Length; i++) { tmpCookie = jar [i]; if (jar.Length > i + 1 && Regex.IsMatch (jar[i], @".*expires\s*=\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)", RegexOptions.IgnoreCase) && Regex.IsMatch (jar[i+1], @"\s\d{2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{4} \d{2}:\d{2}:\d{2} GMT", RegexOptions.IgnoreCase)) { tmpCookie = new StringBuilder (tmpCookie).Append (",").Append (jar [++i]).ToString (); } try { Cookie c = Parse (tmpCookie); // add default values from URI if missing from the string if (c.Path.Length == 0) { c.Path = uri.AbsolutePath; } else if (!uri.AbsolutePath.StartsWith (c.Path)) { string msg = String.Format ("'Path'='{0}' is invalid with URI", c.Path); throw new CookieException (msg); } if (c.Domain.Length == 0) { c.Domain = uri.Host; // don't consider domain "a.b.com" as ".a.b.com" c.ExactDomain = true; } AddCookie (c); } catch (Exception e) { string msg = String.Format ("Could not parse cookies for '{0}'.", uri); throw new CookieException (msg, e); } } } static Cookie Parse (string s) { string [] parts = s.Split (';'); Cookie c = new Cookie (); for (int i = 0; i < parts.Length; i++) { string key, value; int sep = parts[i].IndexOf ('='); if (sep == -1) { key = parts [i].Trim (); value = String.Empty; } else { key = parts [i].Substring (0, sep).Trim (); value = parts [i].Substring (sep + 1).Trim (); } switch (key.ToLowerInvariant ()) { case "path": case "$path": if (c.Path.Length == 0) c.Path = value; break; case "domain": case "$domain": if (c.Domain.Length == 0) { c.Domain = value; // here mono.com means "*.mono.com" c.ExactDomain = false; } break; case "expires": case "$expires": if (c.Expires == DateTime.MinValue) c.Expires = DateTime.SpecifyKind (DateTime.ParseExact (value, @"ddd, dd-MMM-yyyy HH:mm:ss G\MT", CultureInfo.InvariantCulture), DateTimeKind.Utc); break; case "httponly": c.HttpOnly = true; break; case "secure": c.Secure = true; break; default: if (c.Name.Length == 0) { c.Name = key; c.Value = value; } break; } } return c; } } }