Fix bug #311: On LinkedList.Clear, detach each node instead of dropping them en masse.
[mono.git] / mcs / class / System / System.Net / CookieContainer.cs
1 //
2 // System.Net.CookieContainer
3 //
4 // Authors:
5 //      Lawrence Pit (loz@cable.a2000.nl)
6 //      Gonzalo Paniagua Javier (gonzalo@ximian.com)
7 //      Sebastien Pouliot  <sebastien@ximian.com>
8 //
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)
12
13 //
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:
21 // 
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
24 // 
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.
32 //
33
34 using System;
35 using System.Collections;
36 using System.Globalization;
37 using System.Runtime.Serialization;
38 using System.Text;
39 using System.Text.RegularExpressions;
40
41 namespace System.Net 
42 {
43         [Serializable]
44 #if MOONLIGHT
45         #if INSIDE_SYSTEM
46         internal sealed class CookieContainer {
47         #else 
48         public sealed class CookieContainer {
49         #endif
50 #else
51         public class CookieContainer {
52 #endif
53                 public const int DefaultCookieLengthLimit = 4096;
54                 public const int DefaultCookieLimit = 300;
55                 public const int DefaultPerDomainCookieLimit = 20;
56
57                 int capacity = DefaultCookieLimit;
58                 int perDomainCapacity = DefaultPerDomainCookieLimit;
59                 int maxCookieSize = DefaultCookieLengthLimit;
60                 CookieCollection cookies;
61                                 
62                 // ctors
63                 public CookieContainer ()
64                 { 
65                 } 
66         
67                 public CookieContainer (int capacity)
68                 {
69                         if (capacity <= 0)
70                                 throw new ArgumentException ("Must be greater than zero", "Capacity");
71
72                         this.capacity = capacity;
73                 }
74                 
75                 public CookieContainer (int capacity, int perDomainCapacity, int maxCookieSize)
76                         : this (capacity)
77                 {
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,
82                                         capacity));
83
84                         if (maxCookieSize <= 0)
85                                 throw new ArgumentException ("Must be greater than zero", "MaxCookieSize");
86
87                         this.perDomainCapacity = perDomainCapacity;
88                         this.maxCookieSize = maxCookieSize;
89                 }
90
91                 // properties
92                 
93                 public int Count { 
94                         get { return (cookies == null) ? 0 : cookies.Count; }
95                 }
96                 
97                 public int Capacity {
98                         get { return capacity; }
99                         set { 
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,
104                                                 perDomainCapacity));
105                                 capacity = value;
106                         }
107                 }
108                 
109                 public int MaxCookieSize {
110                         get { return maxCookieSize; }
111                         set {
112                                 if (value <= 0)
113                                         throw new ArgumentOutOfRangeException ("value");
114                                 maxCookieSize = value;
115                         }
116                 }
117                 
118                 public int PerDomainCapacity {
119                         get { return perDomainCapacity; }
120                         set {
121                                 if (value != Int32.MaxValue && (value <= 0 || value > capacity))
122                                         throw new ArgumentOutOfRangeException ("value");
123                                 perDomainCapacity = value;
124                         }
125                 }
126                 
127                 public void Add (Cookie cookie) 
128                 {
129                         if (cookie == null)
130                                 throw new ArgumentNullException ("cookie");
131
132                         if (cookie.Domain.Length == 0)
133                                 throw new ArgumentException ("Cookie domain not set.", "cookie.Domain");
134
135                         if (cookie.Value.Length > maxCookieSize)
136                                 throw new CookieException ("value is larger than MaxCookieSize.");
137
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;
146                         
147                         AddCookie (c);
148                 }
149
150                 void AddCookie (Cookie cookie)
151                 {
152                         if (cookies == null)
153                                 cookies = new CookieCollection ();
154
155                         if (cookies.Count >= capacity)
156                                 RemoveOldest (null);
157
158                         // try to avoid counting per-domain
159                         if (cookies.Count >= perDomainCapacity) {
160                                 if (CountDomain (cookie.Domain) >= perDomainCapacity)
161                                         RemoveOldest (cookie.Domain);
162                         }
163
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;
176
177                         cookies.Add (c);
178                         CheckExpiration ();
179
180                 }
181
182                 int CountDomain (string domain)
183                 {
184                         int count = 0;
185                         foreach (Cookie c in cookies) {
186                                 if (CheckDomain (domain, c.Domain, true))
187                                         count++;
188                         }
189                         return count;
190                 }
191
192                 void RemoveOldest (string domain)
193                 {
194                         int n = 0;
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;
200                                         n = i;
201                                 }
202                         }
203                         cookies.List.RemoveAt (n);
204                 }
205
206                 // Only needs to be called from AddCookie (Cookie) and GetCookies (Uri)
207                 void CheckExpiration ()
208                 {
209                         if (cookies == null)
210                                 return;
211
212                         for (int i = cookies.Count - 1; i >= 0; i--) {
213                                 Cookie cookie = cookies [i];
214                                 if (cookie.Expired)
215                                         cookies.List.RemoveAt (i);
216                         }
217                 }
218
219                 public void Add (CookieCollection cookies)
220                 {
221                         if (cookies == null)
222                                 throw new ArgumentNullException ("cookies");
223
224                         foreach (Cookie cookie in cookies)
225                                 Add (cookie);
226                 }
227
228                 void Cook (Uri uri, Cookie cookie)
229                 {
230                         if (String.IsNullOrEmpty (cookie.Name))
231                                 throw new CookieException ("Invalid cookie: name");
232
233                         if (cookie.Value == null)
234                                 throw new CookieException ("Invalid cookie: value");
235
236                         if (uri != null && cookie.Domain.Length == 0)
237                                 cookie.Domain = uri.Host;
238
239                         if (cookie.Version == 0 && String.IsNullOrEmpty (cookie.Path)) {
240                                 if (uri != null) {
241                                         cookie.Path = uri.AbsolutePath;
242                                 } else {
243                                         cookie.Path = "/";
244                                 }
245                         }
246
247                         if (cookie.Port.Length == 0 && uri != null && !uri.IsDefaultPort) {
248                                 cookie.Port = "\"" + uri.Port.ToString () + "\"";
249                         }
250                 }
251
252                 public void Add (Uri uri, Cookie cookie)
253                 {
254                         if (uri == null)
255                                 throw new ArgumentNullException ("uri");
256
257                         if (cookie == null)
258                                 throw new ArgumentNullException ("cookie");
259
260                         if (!cookie.Expired) {
261                                 Cook (uri, cookie);
262                                 AddCookie (cookie);
263                         }
264                 }
265
266                 public void Add (Uri uri, CookieCollection cookies)
267                 {
268                         if (uri == null)
269                                 throw new ArgumentNullException ("uri");
270
271                         if (cookies == null)
272                                 throw new ArgumentNullException ("cookies");
273
274                         foreach (Cookie cookie in cookies) {
275                                 if (!cookie.Expired) {
276                                         Cook (uri, cookie);
277                                         AddCookie (cookie);
278                                 }
279                         }
280                 }               
281
282                 public string GetCookieHeader (Uri uri)
283                 {
284                         if (uri == null)
285                                 throw new ArgumentNullException ("uri");
286
287                         CookieCollection coll = GetCookies (uri);
288                         if (coll.Count == 0)
289                                 return "";
290
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 ("; ");
297                         }
298
299                         if (result.Length > 0)
300                                 result.Length -= 2; // remove trailing semicolon and space
301
302                         return result.ToString ();
303                 }
304
305                 static bool CheckDomain (string domain, string host, bool exact)
306                 {
307                         if (domain.Length == 0)
308                                 return false;
309
310                         if (exact)
311                                 return (String.Compare (host, domain, StringComparison.InvariantCultureIgnoreCase) == 0);
312
313                         // check for allowed sub-domains - without string allocations
314                         if (!host.EndsWith (domain, StringComparison.InvariantCultureIgnoreCase))
315                                 return false;
316                         // mono.com -> www.mono.com is OK but supermono.com NOT OK
317                         if (domain [0] == '.')
318                                 return true;
319                         int p = host.Length - domain.Length - 1;
320                         if (p < 0)
321                                 return false;
322                         return (host [p] == '.');
323                 }
324
325                 public CookieCollection GetCookies (Uri uri)
326                 {
327                         if (uri == null)
328                                 throw new ArgumentNullException ("uri");
329
330                         CheckExpiration ();
331                         CookieCollection coll = new CookieCollection ();
332                         if (cookies == null)
333                                 return coll;
334
335                         foreach (Cookie cookie in cookies) {
336                                 string domain = cookie.Domain;
337                                 if (!CheckDomain (domain, uri.Host, cookie.ExactDomain))
338                                         continue;
339
340                                 if (cookie.Port.Length > 0 && cookie.Ports != null && uri.Port != -1) {
341                                         if (Array.IndexOf (cookie.Ports, uri.Port) == -1)
342                                                 continue;
343                                 }
344
345                                 string path = cookie.Path;
346                                 string uripath = uri.AbsolutePath;
347                                 if (path != "" && path != "/") {
348                                         if (uripath != path) {
349                                                 if (!uripath.StartsWith (path))
350                                                         continue;
351
352                                                 if (path [path.Length - 1] != '/' && uripath.Length > path.Length &&
353                                                     uripath [path.Length] != '/')
354                                                         continue;
355                                         }
356                                 }
357
358                                 if (cookie.Secure && uri.Scheme != "https")
359                                         continue;
360
361                                 coll.Add (cookie);
362                         }
363
364                         coll.Sort ();
365                         return coll;
366                 }
367
368                 public void SetCookies (Uri uri, string cookieHeader)
369                 {
370                         if (uri == null)
371                                 throw new ArgumentNullException ("uri");
372                         
373                         if (cookieHeader == null)
374                                 throw new ArgumentNullException ("cookieHeader");                       
375                         
376                         if (cookieHeader.Length == 0)
377                                 return;
378                         
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 (',');
383                         string tmpCookie;
384                         for (int i = 0; i < jar.Length; i++) {
385                                 tmpCookie = jar [i];
386
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 ();
395                                 }
396
397                                 try {
398                                         Cookie c = Parse (tmpCookie);
399
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);
406                                         }
407
408                                         if (c.Domain.Length == 0) {
409                                                 c.Domain = uri.Host;
410                                                 // don't consider domain "a.b.com" as ".a.b.com"
411                                                 c.ExactDomain = true;
412                                         }
413
414                                         AddCookie (c);
415                                 }
416                                 catch (Exception e) {
417                                         string msg = String.Format ("Could not parse cookies for '{0}'.", uri);
418                                         throw new CookieException (msg, e);
419                                 }
420                         }
421                 }
422
423                 static Cookie Parse (string s)
424                 {
425                         string [] parts = s.Split (';');
426                         Cookie c = new Cookie ();
427                         for (int i = 0; i < parts.Length; i++) {
428                                 string key, value;
429                                 int sep = parts[i].IndexOf ('=');
430                                 if (sep == -1) {
431                                         key = parts [i].Trim ();
432                                         value = String.Empty;
433                                 } else {
434                                         key = parts [i].Substring (0, sep).Trim ();
435                                         value = parts [i].Substring (sep + 1).Trim ();
436                                 }
437
438                                 switch (key.ToLowerInvariant ()) {
439                                 case "path":
440                                 case "$path":
441                                         if (c.Path.Length == 0)
442                                                 c.Path = value;
443                                         break;
444                                 case "domain":
445                                 case "$domain":
446                                         if (c.Domain.Length == 0) {
447                                                 c.Domain = value;
448                                                 // here mono.com means "*.mono.com"
449                                                 c.ExactDomain = false;
450                                         }
451                                         break;
452                                 case "expires":
453                                 case "$expires":
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);
457                                                 break;
458                                 case "httponly":
459                                         c.HttpOnly = true;
460                                         break;
461                                 case "secure":
462                                         c.Secure = true;
463                                         break;
464                                 default:
465                                         if (c.Name.Length == 0) {
466                                                 c.Name = key;
467                                                 c.Value = value;
468                                         }
469                                         break;
470                                 }
471                         }
472                         return c;
473                 }
474         }
475 }
476