2 // System.Net.CookieContainer
5 // Lawrence Pit (loz@cable.a2000.nl)
6 // Gonzalo Paniagua Javier (gonzalo@ximian.com)
7 // Sebastien Pouliot <sebastien@ximian.com>
8 // Marek Safar (marek.safar@gmail.com)
10 // (c) 2003 Ximian, Inc. (http://www.ximian.com)
11 // (c) Copyright 2004 Ximian, Inc. (http://www.ximian.com)
12 // Copyright (C) 2009 Novell, Inc (http://www.novell.com)
13 // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
16 // Permission is hereby granted, free of charge, to any person obtaining
17 // a copy of this software and associated documentation files (the
18 // "Software"), to deal in the Software without restriction, including
19 // without limitation the rights to use, copy, modify, merge, publish,
20 // distribute, sublicense, and/or sell copies of the Software, and to
21 // permit persons to whom the Software is furnished to do so, subject to
22 // the following conditions:
24 // The above copyright notice and this permission notice shall be
25 // included in all copies or substantial portions of the Software.
27 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
29 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
31 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
32 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
33 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 using System.Collections;
38 using System.Globalization;
39 using System.Runtime.Serialization;
41 using System.Text.RegularExpressions;
48 internal sealed class CookieContainer {
50 public sealed class CookieContainer {
53 public class CookieContainer {
55 public const int DefaultCookieLengthLimit = 4096;
56 public const int DefaultCookieLimit = 300;
57 public const int DefaultPerDomainCookieLimit = 20;
59 int capacity = DefaultCookieLimit;
60 int perDomainCapacity = DefaultPerDomainCookieLimit;
61 int maxCookieSize = DefaultCookieLengthLimit;
62 CookieCollection cookies;
65 public CookieContainer ()
69 public CookieContainer (int capacity)
72 throw new ArgumentException ("Must be greater than zero", "Capacity");
74 this.capacity = capacity;
77 public CookieContainer (int capacity, int perDomainCapacity, int maxCookieSize)
80 if (perDomainCapacity != Int32.MaxValue && (perDomainCapacity <= 0 || perDomainCapacity > capacity))
81 throw new ArgumentOutOfRangeException ("perDomainCapacity",
82 string.Format ("PerDomainCapacity must be " +
83 "greater than {0} and less than {1}.", 0,
86 if (maxCookieSize <= 0)
87 throw new ArgumentException ("Must be greater than zero", "MaxCookieSize");
89 this.perDomainCapacity = perDomainCapacity;
90 this.maxCookieSize = maxCookieSize;
96 get { return (cookies == null) ? 0 : cookies.Count; }
100 get { return capacity; }
102 if (value < 0 || (value < perDomainCapacity && perDomainCapacity != Int32.MaxValue))
103 throw new ArgumentOutOfRangeException ("value",
104 string.Format ("Capacity must be greater " +
105 "than {0} and less than {1}.", 0,
111 public int MaxCookieSize {
112 get { return maxCookieSize; }
115 throw new ArgumentOutOfRangeException ("value");
116 maxCookieSize = value;
120 public int PerDomainCapacity {
121 get { return perDomainCapacity; }
123 if (value != Int32.MaxValue && (value <= 0 || value > capacity))
124 throw new ArgumentOutOfRangeException ("value");
125 perDomainCapacity = value;
129 public void Add (Cookie cookie)
132 throw new ArgumentNullException ("cookie");
134 if (cookie.Domain.Length == 0)
135 throw new ArgumentException ("Cookie domain not set.", "cookie.Domain");
137 if (cookie.Value.Length > maxCookieSize)
138 throw new CookieException ("value is larger than MaxCookieSize.");
140 // .NET's Add (Cookie) is fundamentally broken and does not copy properties
141 // like Secure, HttpOnly and Expires so we clone the parts that .NET
142 // does keep before calling AddCookie
143 Cookie c = new Cookie (cookie.Name, cookie.Value);
144 c.Path = (cookie.Path.Length == 0) ? "/" : cookie.Path;
145 c.Domain = cookie.Domain;
146 c.ExactDomain = cookie.ExactDomain;
147 c.Version = cookie.Version;
152 void AddCookie (Cookie cookie)
155 cookies = new CookieCollection ();
157 if (cookies.Count >= capacity)
160 // try to avoid counting per-domain
161 if (cookies.Count >= perDomainCapacity) {
162 if (CountDomain (cookie.Domain) >= perDomainCapacity)
163 RemoveOldest (cookie.Domain);
166 // clone the important parts of the cookie
167 Cookie c = new Cookie (cookie.Name, cookie.Value);
168 c.Path = (cookie.Path.Length == 0) ? "/" : cookie.Path;
169 c.Domain = cookie.Domain;
170 c.ExactDomain = cookie.ExactDomain;
171 c.Version = cookie.Version;
172 c.Expires = cookie.Expires;
173 c.CommentUri = cookie.CommentUri;
174 c.Comment = cookie.Comment;
175 c.Discard = cookie.Discard;
176 c.HttpOnly = cookie.HttpOnly;
177 c.Secure = cookie.Secure;
184 int CountDomain (string domain)
187 foreach (Cookie c in cookies) {
188 if (CheckDomain (domain, c.Domain, true))
194 void RemoveOldest (string domain)
197 DateTime oldest = DateTime.MaxValue;
198 for (int i = 0; i < cookies.Count; i++) {
199 Cookie c = cookies [i];
200 if ((c.TimeStamp < oldest) && ((domain == null) || (domain == c.Domain))) {
201 oldest = c.TimeStamp;
205 cookies.List.RemoveAt (n);
208 // Only needs to be called from AddCookie (Cookie) and GetCookies (Uri)
209 void CheckExpiration ()
214 for (int i = cookies.Count - 1; i >= 0; i--) {
215 Cookie cookie = cookies [i];
217 cookies.List.RemoveAt (i);
221 public void Add (CookieCollection cookies)
224 throw new ArgumentNullException ("cookies");
226 foreach (Cookie cookie in cookies)
230 void Cook (Uri uri, Cookie cookie)
232 if (String.IsNullOrEmpty (cookie.Name))
233 throw new CookieException ("Invalid cookie: name");
235 if (cookie.Value == null)
236 throw new CookieException ("Invalid cookie: value");
238 if (uri != null && cookie.Domain.Length == 0)
239 cookie.Domain = uri.Host;
241 if (cookie.Version == 0 && String.IsNullOrEmpty (cookie.Path)) {
243 cookie.Path = uri.AbsolutePath;
249 if (cookie.Port.Length == 0 && uri != null && !uri.IsDefaultPort) {
250 cookie.Ports = new [] { uri.Port };
254 public void Add (Uri uri, Cookie cookie)
257 throw new ArgumentNullException ("uri");
260 throw new ArgumentNullException ("cookie");
262 if (!cookie.Expired) {
268 public void Add (Uri uri, CookieCollection cookies)
271 throw new ArgumentNullException ("uri");
274 throw new ArgumentNullException ("cookies");
276 foreach (Cookie cookie in cookies) {
277 if (!cookie.Expired) {
284 public string GetCookieHeader (Uri uri)
287 throw new ArgumentNullException ("uri");
289 CookieCollection coll = GetCookies (uri);
293 StringBuilder result = new StringBuilder ();
294 foreach (Cookie cookie in coll) {
295 // don't include the domain since it can be infered from the URI
296 // include empty path as '/'
297 result.Append (cookie.ToString (uri));
298 result.Append ("; ");
301 if (result.Length > 0)
302 result.Length -= 2; // remove trailing semicolon and space
304 return result.ToString ();
307 static bool CheckDomain (string domain, string host, bool exact)
309 if (domain.Length == 0)
313 return (String.Compare (host, domain, StringComparison.InvariantCultureIgnoreCase) == 0);
315 // check for allowed sub-domains - without string allocations
316 if (!host.EndsWith (domain, StringComparison.InvariantCultureIgnoreCase))
318 // mono.com -> www.mono.com is OK but supermono.com NOT OK
319 if (domain [0] == '.')
321 int p = host.Length - domain.Length - 1;
324 return (host [p] == '.');
327 public CookieCollection GetCookies (Uri uri)
330 throw new ArgumentNullException ("uri");
333 CookieCollection coll = new CookieCollection ();
337 foreach (Cookie cookie in cookies) {
338 string domain = cookie.Domain;
339 if (!CheckDomain (domain, uri.Host, cookie.ExactDomain))
342 if (cookie.Port.Length > 0 && cookie.Ports != null && uri.Port != -1) {
343 if (Array.IndexOf (cookie.Ports, uri.Port) == -1)
347 string path = cookie.Path;
348 string uripath = uri.AbsolutePath;
349 if (path != "" && path != "/") {
350 if (uripath != path) {
351 if (!uripath.StartsWith (path))
354 if (path [path.Length - 1] != '/' && uripath.Length > path.Length &&
355 uripath [path.Length] != '/')
360 if (cookie.Secure && uri.Scheme != "https")
370 public void SetCookies (Uri uri, string cookieHeader)
373 throw new ArgumentNullException ("uri");
375 if (cookieHeader == null)
376 throw new ArgumentNullException ("cookieHeader");
378 if (cookieHeader.Length == 0)
381 // Cookies must be separated by ',' (like documented on MSDN)
382 // but expires uses DAY, DD-MMM-YYYY HH:MM:SS GMT, so simple ',' search is wrong.
383 // See http://msdn.microsoft.com/en-us/library/aa384321%28VS.85%29.aspx
384 string [] jar = cookieHeader.Split (',');
386 for (int i = 0; i < jar.Length; i++) {
389 if (jar.Length > i + 1
390 && Regex.IsMatch (jar[i],
391 @".*expires\s*=\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)",
392 RegexOptions.IgnoreCase)
393 && Regex.IsMatch (jar[i+1],
394 @"\s\d{2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{4} \d{2}:\d{2}:\d{2} GMT",
395 RegexOptions.IgnoreCase)) {
396 tmpCookie = new StringBuilder (tmpCookie).Append (",").Append (jar [++i]).ToString ();
400 Cookie c = Parse (tmpCookie);
402 // add default values from URI if missing from the string
403 if (c.Path.Length == 0) {
404 c.Path = uri.AbsolutePath;
405 } else if (!uri.AbsolutePath.StartsWith (c.Path)) {
406 string msg = String.Format ("'Path'='{0}' is invalid with URI", c.Path);
407 throw new CookieException (msg);
410 if (c.Domain.Length == 0) {
412 // don't consider domain "a.b.com" as ".a.b.com"
413 c.ExactDomain = true;
418 catch (Exception e) {
419 string msg = String.Format ("Could not parse cookies for '{0}'.", uri);
420 throw new CookieException (msg, e);
425 static Cookie Parse (string s)
427 string [] parts = s.Split (';');
428 Cookie c = new Cookie ();
429 for (int i = 0; i < parts.Length; i++) {
431 int sep = parts[i].IndexOf ('=');
433 key = parts [i].Trim ();
434 value = String.Empty;
436 key = parts [i].Substring (0, sep).Trim ();
437 value = parts [i].Substring (sep + 1).Trim ();
440 switch (key.ToLowerInvariant ()) {
443 if (c.Path.Length == 0)
448 if (c.Domain.Length == 0) {
450 // here mono.com means "*.mono.com"
451 c.ExactDomain = false;
456 if (c.Expires == DateTime.MinValue)
457 c.Expires = DateTime.SpecifyKind (DateTime.ParseExact (value,
458 @"ddd, dd-MMM-yyyy HH:mm:ss G\MT", CultureInfo.InvariantCulture), DateTimeKind.Utc);
467 if (c.Name.Length == 0) {