2 // System.Net.CookieContainer
5 // Lawrence Pit (loz@cable.a2000.nl)
6 // Gonzalo Paniagua Javier (gonzalo@ximian.com)
7 // Sebastien Pouliot <sebastien@ximian.com>
9 // (c) 2003 Ximian, Inc. (http://www.ximian.com)
10 // (c) Copyright 2004 Ximian, Inc. (http://www.ximian.com)
11 // Copyright (C) 2009 Novell, Inc (http://www.novell.com)
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
25 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35 using System.Collections;
36 using System.Globalization;
37 using System.Runtime.Serialization;
39 using System.Text.RegularExpressions;
46 internal sealed class CookieContainer {
48 public sealed class CookieContainer {
51 public class CookieContainer {
53 public const int DefaultCookieLengthLimit = 4096;
54 public const int DefaultCookieLimit = 300;
55 public const int DefaultPerDomainCookieLimit = 20;
57 int capacity = DefaultCookieLimit;
58 int perDomainCapacity = DefaultPerDomainCookieLimit;
59 int maxCookieSize = DefaultCookieLengthLimit;
60 CookieCollection cookies;
63 public CookieContainer ()
67 public CookieContainer (int capacity)
70 throw new ArgumentException ("Must be greater than zero", "Capacity");
72 this.capacity = capacity;
75 public CookieContainer (int capacity, int perDomainCapacity, int maxCookieSize)
78 if (perDomainCapacity != Int32.MaxValue && (perDomainCapacity <= 0 || perDomainCapacity > capacity))
79 throw new ArgumentOutOfRangeException ("perDomainCapacity",
80 string.Format ("PerDomainCapacity must be " +
81 "greater than {0} and less than {1}.", 0,
84 if (maxCookieSize <= 0)
85 throw new ArgumentException ("Must be greater than zero", "MaxCookieSize");
87 this.perDomainCapacity = perDomainCapacity;
88 this.maxCookieSize = maxCookieSize;
94 get { return (cookies == null) ? 0 : cookies.Count; }
98 get { return capacity; }
100 if (value < 0 || (value < perDomainCapacity && perDomainCapacity != Int32.MaxValue))
101 throw new ArgumentOutOfRangeException ("value",
102 string.Format ("Capacity must be greater " +
103 "than {0} and less than {1}.", 0,
109 public int MaxCookieSize {
110 get { return maxCookieSize; }
113 throw new ArgumentOutOfRangeException ("value");
114 maxCookieSize = value;
118 public int PerDomainCapacity {
119 get { return perDomainCapacity; }
121 if (value != Int32.MaxValue && (value <= 0 || value > capacity))
122 throw new ArgumentOutOfRangeException ("value");
123 perDomainCapacity = value;
127 public void Add (Cookie cookie)
130 throw new ArgumentNullException ("cookie");
132 if (cookie.Domain.Length == 0)
133 throw new ArgumentException ("Cookie domain not set.", "cookie.Domain");
135 if (cookie.Value.Length > maxCookieSize)
136 throw new CookieException ("value is larger than MaxCookieSize.");
138 // .NET's Add (Cookie) is fundamentally broken and does not copy properties
139 // like Secure, HttpOnly and Expires so we clone the parts that .NET
140 // does keep before calling AddCookie
141 Cookie c = new Cookie (cookie.Name, cookie.Value);
142 c.Path = (cookie.Path.Length == 0) ? "/" : cookie.Path;
143 c.Domain = cookie.Domain;
144 c.ExactDomain = cookie.ExactDomain;
145 c.Version = cookie.Version;
150 void AddCookie (Cookie cookie)
153 cookies = new CookieCollection ();
155 if (cookies.Count >= capacity)
158 // try to avoid counting per-domain
159 if (cookies.Count >= perDomainCapacity) {
160 if (CountDomain (cookie.Domain) >= perDomainCapacity)
161 RemoveOldest (cookie.Domain);
164 // clone the important parts of the cookie
165 Cookie c = new Cookie (cookie.Name, cookie.Value);
166 c.Path = (cookie.Path.Length == 0) ? "/" : cookie.Path;
167 c.Domain = cookie.Domain;
168 c.ExactDomain = cookie.ExactDomain;
169 c.Version = cookie.Version;
170 c.Expires = cookie.Expires;
171 c.CommentUri = cookie.CommentUri;
172 c.Comment = cookie.Comment;
173 c.Discard = cookie.Discard;
174 c.HttpOnly = cookie.HttpOnly;
175 c.Secure = cookie.Secure;
182 int CountDomain (string domain)
185 foreach (Cookie c in cookies) {
186 if (CheckDomain (domain, c.Domain, true))
192 void RemoveOldest (string domain)
195 DateTime oldest = DateTime.MaxValue;
196 for (int i = 0; i < cookies.Count; i++) {
197 Cookie c = cookies [i];
198 if ((c.TimeStamp < oldest) && ((domain == null) || (domain == c.Domain))) {
199 oldest = c.TimeStamp;
203 cookies.List.RemoveAt (n);
206 // Only needs to be called from AddCookie (Cookie) and GetCookies (Uri)
207 void CheckExpiration ()
212 for (int i = cookies.Count - 1; i >= 0; i--) {
213 Cookie cookie = cookies [i];
215 cookies.List.RemoveAt (i);
219 public void Add (CookieCollection cookies)
222 throw new ArgumentNullException ("cookies");
224 foreach (Cookie cookie in cookies)
228 void Cook (Uri uri, Cookie cookie)
230 if (String.IsNullOrEmpty (cookie.Name))
231 throw new CookieException ("Invalid cookie: name");
233 if (cookie.Value == null)
234 throw new CookieException ("Invalid cookie: value");
236 if (uri != null && cookie.Domain.Length == 0)
237 cookie.Domain = uri.Host;
239 if (cookie.Version == 0 && String.IsNullOrEmpty (cookie.Path)) {
241 cookie.Path = uri.AbsolutePath;
247 if (cookie.Port.Length == 0 && uri != null && !uri.IsDefaultPort) {
248 cookie.Port = "\"" + uri.Port.ToString () + "\"";
252 public void Add (Uri uri, Cookie cookie)
255 throw new ArgumentNullException ("uri");
258 throw new ArgumentNullException ("cookie");
260 if (!cookie.Expired) {
266 public void Add (Uri uri, CookieCollection cookies)
269 throw new ArgumentNullException ("uri");
272 throw new ArgumentNullException ("cookies");
274 foreach (Cookie cookie in cookies) {
275 if (!cookie.Expired) {
282 public string GetCookieHeader (Uri uri)
285 throw new ArgumentNullException ("uri");
287 CookieCollection coll = GetCookies (uri);
291 StringBuilder result = new StringBuilder ();
292 foreach (Cookie cookie in coll) {
293 // don't include the domain since it can be infered from the URI
294 // include empty path as '/'
295 result.Append (cookie.ToString (uri));
296 result.Append ("; ");
299 if (result.Length > 0)
300 result.Length -= 2; // remove trailing semicolon and space
302 return result.ToString ();
305 static bool CheckDomain (string domain, string host, bool exact)
307 if (domain.Length == 0)
311 return (String.Compare (host, domain, StringComparison.InvariantCultureIgnoreCase) == 0);
313 // check for allowed sub-domains - without string allocations
314 if (!host.EndsWith (domain, StringComparison.InvariantCultureIgnoreCase))
316 // mono.com -> www.mono.com is OK but supermono.com NOT OK
317 if (domain [0] == '.')
319 int p = host.Length - domain.Length - 1;
322 return (host [p] == '.');
325 public CookieCollection GetCookies (Uri uri)
328 throw new ArgumentNullException ("uri");
331 CookieCollection coll = new CookieCollection ();
335 foreach (Cookie cookie in cookies) {
336 string domain = cookie.Domain;
337 if (!CheckDomain (domain, uri.Host, cookie.ExactDomain))
340 if (cookie.Port.Length > 0 && cookie.Ports != null && uri.Port != -1) {
341 if (Array.IndexOf (cookie.Ports, uri.Port) == -1)
345 string path = cookie.Path;
346 string uripath = uri.AbsolutePath;
347 if (path != "" && path != "/") {
348 if (uripath != path) {
349 if (!uripath.StartsWith (path))
352 if (path [path.Length - 1] != '/' && uripath.Length > path.Length &&
353 uripath [path.Length] != '/')
358 if (cookie.Secure && uri.Scheme != "https")
368 public void SetCookies (Uri uri, string cookieHeader)
371 throw new ArgumentNullException ("uri");
373 if (cookieHeader == null)
374 throw new ArgumentNullException ("cookieHeader");
376 if (cookieHeader.Length == 0)
379 // Cookies must be separated by ',' (like documented on MSDN)
380 // but expires uses DAY, DD-MMM-YYYY HH:MM:SS GMT, so simple ',' search is wrong.
381 // See http://msdn.microsoft.com/en-us/library/aa384321%28VS.85%29.aspx
382 string [] jar = cookieHeader.Split (',');
384 for (int i = 0; i < jar.Length; i++) {
387 if (jar.Length > i + 1
388 && Regex.IsMatch (jar[i],
389 @".*expires\s*=\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)",
390 RegexOptions.IgnoreCase)
391 && Regex.IsMatch (jar[i+1],
392 @"\s\d{2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{4} \d{2}:\d{2}:\d{2} GMT",
393 RegexOptions.IgnoreCase)) {
394 tmpCookie = new StringBuilder (tmpCookie).Append (",").Append (jar [++i]).ToString ();
398 Cookie c = Parse (tmpCookie);
400 // add default values from URI if missing from the string
401 if (c.Path.Length == 0) {
402 c.Path = uri.AbsolutePath;
403 } else if (!uri.AbsolutePath.StartsWith (c.Path)) {
404 string msg = String.Format ("'Path'='{0}' is invalid with URI", c.Path);
405 throw new CookieException (msg);
408 if (c.Domain.Length == 0) {
410 // don't consider domain "a.b.com" as ".a.b.com"
411 c.ExactDomain = true;
416 catch (Exception e) {
417 string msg = String.Format ("Could not parse cookies for '{0}'.", uri);
418 throw new CookieException (msg, e);
423 static Cookie Parse (string s)
425 string [] parts = s.Split (';');
426 Cookie c = new Cookie ();
427 for (int i = 0; i < parts.Length; i++) {
429 int sep = parts[i].IndexOf ('=');
431 key = parts [i].Trim ();
432 value = String.Empty;
434 key = parts [i].Substring (0, sep).Trim ();
435 value = parts [i].Substring (sep + 1).Trim ();
438 switch (key.ToLowerInvariant ()) {
441 if (c.Path.Length == 0)
446 if (c.Domain.Length == 0) {
448 // here mono.com means "*.mono.com"
449 c.ExactDomain = false;
454 if (c.Expires == DateTime.MinValue)
455 c.Expires = DateTime.SpecifyKind (DateTime.ParseExact (value,
456 @"ddd, dd-MMM-yyyy HH:mm:ss G\MT", CultureInfo.InvariantCulture), DateTimeKind.Utc);
465 if (c.Name.Length == 0) {