Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: com/sonalb/net/http/cookie/RFC2965CookieParser.java


1   /*
2    * -*- mode: java; c-basic-indent: 4; indent-tabs-mode: nil -*-
3    * :indentSize=4:noTabs=true:tabSize=4:indentOnTab=true:indentOnEnter=true:mode=java:
4    * ex: set tabstop=4 expandtab:
5    *
6    * MrPostman - webmail <-> email gateway
7    * Copyright (C) 2002-2003 MrPostman Development Group
8    * Projectpage: http://mrbook.org/mrpostman/
9    *
10   *
11   * This program is free software; you can redistribute it and/or modify
12   * it under the terms of the GNU General Public License as published by
13   * the Free Software Foundation; either version 2 of the License, or
14   * (at your option) any later version.
15   *
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   * In particular, this implies that users are responsible for
21   * using MrPostman after reading the terms and conditions given
22   * by their web-mail provider.
23   *
24   * You should have received a copy of the GNU General Public License
25   * Named LICENSE in the base directory of this distribution,
26   * if not, write to the Free Software
27   * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28   */
29  
30  package com.sonalb.net.http.cookie;
31  
32  import com.sonalb.Utils;
33  
34  import com.sonalb.net.http.Header;
35  import com.sonalb.net.http.HeaderEntry;
36  
37  import java.net.InetAddress;
38  import java.net.MalformedURLException;
39  import java.net.URL;
40  
41  import java.util.Collections;
42  import java.util.Date;
43  import java.util.Iterator;
44  import java.util.StringTokenizer;
45  import java.util.Vector;
46  import java.util.logging.Level;
47  import java.util.logging.Logger;
48  
49  
50  /**
51  
52   * Implementation for CookieParser that conforms to cookie specification RFC-2965.
53  
54   * @author    Sonal Bansal
55  
56   */
57  public class RFC2965CookieParser implements CookieParser {
58      public static final String CVSID = "$Id: RFC2965CookieParser.java,v 1.6 2003/02/09 23:38:11 lbruand Exp $";
59      private static Logger logger = Logger.getLogger("com.sonalb.net.http.cookie.RFC2965CookieParser");
60  
61      public Header getCookieHeaders(CookieJar cj) {
62          if (cj == null) {
63              throw new IllegalArgumentException("Null CookieJar");
64          }
65  
66          if (cj.isEmpty()) {
67              return (null);
68          }
69  
70          CookieJar eligibleV1Cookies = sortCookiesByPathSpecificity(cj.getVersionCookies("1"));
71  
72          CookieJar eligibleV0Cookies = sortCookiesByPathSpecificity(cj.getVersionCookies("0"));
73  
74          Header headers = new Header();
75  
76          headers.add("Cookie2", "1");
77  
78          StringBuffer sb;
79  
80          boolean bFirstElement;
81  
82          Iterator iter;
83  
84          if (!eligibleV1Cookies.isEmpty()) {
85              sb = new StringBuffer();
86  
87              bFirstElement = true;
88  
89              iter = eligibleV1Cookies.iterator();
90  
91              while (iter.hasNext()) {
92                  if (bFirstElement) {
93                      sb.append(toCookieHeaderForm((Cookie) iter.next(), true));
94  
95                      bFirstElement = false;
96                  } else {
97                      sb.append(toCookieHeaderForm((Cookie) iter.next(), false));
98                  }
99  
100                 sb.append(";");
101             }
102 
103             sb.deleteCharAt(sb.length() - 1);
104 
105             headers.add("Cookie", sb.toString());
106         }
107 
108         if (!eligibleV0Cookies.isEmpty()) {
109             sb = new StringBuffer();
110 
111             bFirstElement = true;
112 
113             iter = eligibleV0Cookies.iterator();
114 
115             while (iter.hasNext()) {
116                 if (bFirstElement) {
117                     sb.append(toCookieHeaderForm((Cookie) iter.next(), true));
118 
119                     bFirstElement = false;
120                 } else {
121                     sb.append(toCookieHeaderForm((Cookie) iter.next(), false));
122                 }
123 
124                 sb.append("; ");
125             }
126 
127             sb.deleteCharAt(sb.length() - 1);
128 
129             sb.deleteCharAt(sb.length() - 1);
130 
131             headers.add("Cookie", sb.toString());
132         }
133 
134         return (headers);
135     }
136 
137     public boolean allowedCookie(Cookie c, URL url) {
138         try {
139             return (allowedCookie(c, url, true));
140         } catch (MalformedCookieException mce) {
141             return (false);
142         }
143     }
144 
145     public CookieJar parseCookies(Header header, URL url)
146         throws MalformedCookieException {
147         if ((header == null) || header.isEmpty()) {
148             throw new IllegalArgumentException("No Headers");
149         }
150 
151         CookieJar cj = new CookieJar();
152 
153         if (header.containsKey("set-cookie")) {
154             logger.fine("Client.getCookies(): There are set-cookie headers.");
155 
156             cj.addAll(parseSetCookieV0(header, url, false));
157         }
158 
159         if (header.containsKey("set-cookie2") || header.containsKey("set-cookie")) {
160             logger.fine("Client.getCookies(): There are set-cookie2 headers.");
161 
162             cj.addAll(parseSetCookieV1(header, url, false));
163         }
164 
165         logger.fine("Client.getCookies(): Parsed. JAR=" + cj.toString());
166 
167         return (cj);
168     }
169 
170     /**
171 
172      * Determines whether a <code>Cookie</code> is eligible to be sent alongwith the request
173 
174      * for a given <code>URL</code>. This method may or may not take into consideration the
175 
176      * lifetime (indicated by cookie parameters). It discerns between Version 0 (Netscape)
177 
178      * and Version 1 (RFC 2965) cookies. For Version 0 cookies, the relevant portion of
179 
180      * Netscape's draft:<p>
181 
182      * "When searching the cookie list for valid cookies, a comparison of the domain attributes
183 
184      * of the cookie is made with the Internet domain name of the host from which the URL will
185 
186      * be fetched. If there is a tail match, then the cookie will go through path matching to
187 
188      * see if it should be sent."
189 
190      * <p>
191 
192      * For Version 1 cookies, the relevant portion of Netscape's draft:<p>
193 
194      * "The user agent applies the following rules to choose applicable
195 
196      * cookie-values to send in Cookie request headers from among all the
197 
198      * cookies it has received.<p>
199 
200      * Domain Selection<br>
201 
202      * The origin server's effective host name MUST domain-match the Domain
203 
204      * attribute of the cookie.<p>
205 
206      * Port Selection<br>
207 
208      * There are three possible behaviors, depending on the Port
209 
210      * attribute in the Set-Cookie2 response header:<p>
211 
212      * &nbsp;&nbsp;&nbsp;&nbsp;1. By default (no Port attribute), the cookie MAY be sent to
213 
214      * any port.<br>
215 
216      * &nbsp;&nbsp;&nbsp;&nbsp;2. If the attribute is present but has no value (e.g., Port), the
217 
218      * cookie MUST only be sent to the request-port it was received from.<br>
219 
220      * &nbsp;&nbsp;&nbsp;&nbsp;3. If the attribute has a port-list, the cookie MUST only be
221 
222      * returned if the new request-port is one of those listed in port-list.<p>
223 
224      * Path Selection<br>
225 
226      * The request-URI MUST path-match the Path attribute of the cookie.<p>
227 
228      * Max-Age Selection<br>
229 
230      * Cookies that have expired should have been discarded and thus are
231 
232      * not forwarded to an origin server."
233 
234      * <p>
235 
236      * Note: For both Versions 0 and 1 cookies, if the cookie is marked as "secure", it can be sent
237 
238      * only over a secure transport. This implementation considers HTTPS and SHTTP protocols as secure.
239 
240      *
241 
242      * @see #pathMatch
243 
244      * @see #tailMatch
245 
246      * @see #domainMatch
247 
248      * @see #portMatch
249 
250      * @see Cookie#hasExpired
251 
252      * @see Cookie#isValid
253 
254      * @param c the <code>Cookie</code> to be tested.
255 
256      * @param url the <code>URL</code> for which to test.
257 
258      * @param bRespectExpires whether or not to take expiry information into consideration.
259 
260      * @returns <code>true</code> if the cookie can be sent to the request-url; false otherwise. Also,
261 
262      *         <code>false</code> if either <code>url</code> or <code>c</code> are null.
263 
264      * @throws IllegalArgumentException Thrown if the input <code>Cookie</code> is not valid, that is,
265 
266      *                     it is incomplete.
267 
268      */
269     public boolean sendCookieWithURL(Cookie c, URL url, boolean bRespectExpires) {
270         if ((url == null) || (c == null)) {
271             logger.fine("RFC2965CookieParser.sendCookieWithURL(): URL or Cookie is Null");
272 
273             return (false);
274         } else if (!c.isValid()) {
275             logger.fine("RFC2965CookieParser.sendCookieWithURL(): Invalid Cookie");
276 
277             throw new IllegalArgumentException("Invalid/Bad cookie.");
278         }
279 
280         if (c.isSecure()) {
281             String protocol = url.getProtocol();
282 
283             if (Utils.isNullOrWhiteSpace(protocol)) {
284                 return (false);
285             }
286 
287             protocol = protocol.toLowerCase();
288 
289             if (!("https".equals(protocol) || "shttp".equals(protocol))) {
290                 return (false);
291             }
292         }
293 
294         String domain = c.getDomain();
295 
296         String path = c.getPath();
297 
298         if ("0".equals(c.getVersion())) {
299             return (tailMatch(url, domain) && pathMatch(url, path) && (bRespectExpires ? (!c.hasExpired()) : true));
300         } else {
301             String portList = c.getPortList();
302 
303             return (domainMatch(url, domain) && portMatch(url, portList) && pathMatch(url, path)
304             && (bRespectExpires ? (!c.hasExpired()) : true));
305         }
306     }
307 
308     // ******************************************************************
309     // Methods for internal use follow
310     // ******************************************************************
311 
312     /**
313 
314      * Sorts the <code>Cookie</code>s in the input <code>CookieJar</code>, with the cookie
315 
316      * having most specific path attribute first. This is used while determining the order in
317 
318      * which cookies must be sent back to the server.<p>
319 
320      * Note: The input <code>CookieJar</code> is NOT modified, instead, a new CookieJar
321 
322      * is returned with the sorted Cookies.
323 
324      *
325 
326      * @param cj the CookieJar with the cookies to be sorted.
327 
328      * @returns the new sorted CookieJar, or the input CookieJar <code>cj</code> if <code>cj</code> is null,
329 
330      *         empty, or has only one cookie.
331 
332      * @see CookieJar
333 
334      */
335     public static CookieJar sortCookiesByPathSpecificity(CookieJar cj) {
336         if ((cj == null) || cj.isEmpty() || (cj.size() == 1)) {
337             return (cj);
338         }
339 
340         Vector v = new Vector();
341 
342         v.addAll(cj);
343 
344         Collections.sort(v);
345 
346         CookieJar sortedCJ = new CookieJar(v);
347 
348         return (sortedCJ);
349     }
350 
351     /**
352 
353      * Performs "path matching" of URL to cookie path, as specified by RFC 2965. The <code>URL</code>'s path
354 
355      * is obtained by <code>URL.getPath()</code>. The relevant portion of RFC 2965 :<p>
356 
357      * "For two strings that represent paths, P1 and P2, P1 path-matches P2 if P2 is a prefix of P1 (including the case
358 
359      * where P1 and P2 string-compare equal). Thus, the string /tec/waldo path-matches /tec."
360 
361      *
362 
363      * @see URL
364 
365      * @param url the <code>URL</code> which must be matched.
366 
367      * @param path the cookie path.
368 
369      * @return <code>true</code> if the URL path-matched the cookie path; <code>false</code> otherwise.
370 
371      */
372     public static boolean pathMatch(URL url, String path) {
373         logger.fine("RFC2965CookieParser.pathMatch(): URL=" + url + ",PATH=" + path);
374 
375         String upath = url.getPath();
376 
377         if (Utils.isNullOrWhiteSpace(upath)) {
378             upath = "/";
379         }
380 
381         logger.fine("RFC2965CookieParser.pathMatch(): URL Path=" + upath);
382 
383         if (upath.equals(path) || upath.startsWith(path)) {
384             logger.fine("RFC2965CookieParser.pathMatch(): Match TRUE");
385 
386             return (true);
387         }
388 
389         logger.fine("RFC2965CookieParser.pathMatch(): Match FALSE");
390 
391         return (false);
392     }
393 
394     /**
395 
396      * Performs "tail matching" of URL host/domain to cookie domain, for Version 0 cookies as specified by Netscape's draft.
397 
398      * The <code>URL</code>'s host is obtained by <code>URL.getHost()</code>. The relevant portion of Netscape's draft :<p>
399 
400      * ""Tail matching" means that domain attribute is matched against the tail of the fully qualified domain name of the host.
401 
402      * A domain attribute of "acme.com" would match host names "anvil.acme.com" as well as "shipping.crate.acme.com".<p>
403 
404      * Only hosts within the specified domain can set a cookie for a domain and domains must have at least two (2) or three (3) periods
405 
406      * in them to prevent domains of the form: ".com", ".edu", and "va.us". Any domain that fails within one of the seven
407 
408      * special top level domains listed below only require two periods. Any other domain requires at least three.
409 
410      * The seven special top level domains are: "COM", "EDU", "NET", "ORG", "GOV", "MIL", and "INT"".
411 
412      *
413 
414      * @see URL
415 
416      * @param url the <code>URL</code> which must be matched.
417 
418      * @param domain the cookie domain.
419 
420      * @return <code>true</code> if the URL tail-matched the cookie domain; <code>false</code> otherwise.
421 
422      */
423     public static boolean tailMatch(URL url, String domain) {
424         logger.fine("RFC2965CookieParser.tailMatch(): URL=" + url + ",DOMAIN=" + domain);
425 
426         String host = url.getHost();
427 
428         if (Utils.isNullOrWhiteSpace(host)) {
429             logger.fine("RFC2965CookieParser.tailMatch(): Match TRUE");
430 
431             return (false);
432         }
433 
434         if (host.indexOf('.') == -1) {
435             host += ".local";
436 
437             logger.fine("RFC2965CookieParser.tailMatch(): Match " + host.toLowerCase().endsWith(domain.toLowerCase()));
438 
439             return (host.toLowerCase().endsWith(domain.toLowerCase()));
440         }
441 
442         String[] specialTLDs = {"com", "edu", "net", "org", "gov", "mil", "int"};
443 
444         int dots = countTheDots(domain);
445 
446         String tld = domain.substring(domain.lastIndexOf('.') + 1);
447 
448         if (Utils.isInArray(tld.toLowerCase(), specialTLDs)) {
449             if (dots >= 2) {
450                 logger.fine("RFC2965CookieParser.tailMatch(): Match "
451                     + host.toLowerCase().endsWith(domain.toLowerCase()));
452 
453                 return (host.toLowerCase().endsWith(domain.toLowerCase()));
454             }
455         } else {
456             if (dots >= 3) {
457                 logger.fine("RFC2965CookieParser.tailMatch(): Match "
458                     + host.toLowerCase().endsWith(domain.toLowerCase()));
459 
460                 return (host.toLowerCase().endsWith(domain.toLowerCase()));
461             }
462         }
463 
464         logger.fine("RFC2965CookieParser.tailMatch(): Match FALSE");
465 
466         return (false);
467     }
468 
469     /**
470 
471      * Performs "domain matching" of URL host/domain to cookie domain, for Version 1 cookies as specified by RFC 2965.
472 
473      * The <code>URL</code>'s host is obtained by <code>URL.getHost()</code>. The relevant portion of RFC 2965 :<p>
474 
475      * "Host names can be specified either as an IP address or a HDN [host domain name] string.
476 
477      * Sometimes we compare one host name with another.  (Such comparisons SHALL be case-insensitive.)
478 
479      * Host A's name domain-matches host B's if<p>
480 
481      * <pre>
482 
483      *     *  their host name strings string-compare equal; or
484 
485      *     *  A is a HDN string and has the form NB, where N is a non-empty
486 
487      *        name string, B has the form .B', and B' is a HDN string.
488 
489      *        (So, x.y.com domain-matches .Y.com but not Y.com.)</pre>
490 
491      * Note that domain-match is not a commutative operation: a.b.c.com
492 
493      * domain-matches .c.com, but not the reverse."
494 
495      *
496 
497      * @see URL
498 
499      * @param url the <code>URL</code> which must be matched.
500 
501      * @param domain the cookie domain.
502 
503      * @return <code>true</code> if the URL domain-matched the cookie domain; <code>false</code> otherwise.
504 
505      */
506     public static boolean domainMatch(URL url, String domain) {
507         logger.fine("RFC2965CookieParser.domainMatch(): URL=" + url + ",DOMAIN=" + domain);
508 
509         try {
510             String host = url.getHost();
511 
512             logger.fine("RFC2965CookieParser.domainMatch(): URL host=" + host);
513 
514             if (Utils.isNullOrWhiteSpace(host)) {
515                 logger.fine("RFC2965CookieParser.domainMatch(): Null host. FALSE.");
516 
517                 return (false);
518             }
519 
520             if (host.indexOf('.') == -1) {
521                 host += ".local";
522             }
523 
524             logger.fine("RFC2965CookieParser.domainMatch(): Equivalent host=" + host);
525 
526             if (host.equalsIgnoreCase(domain)) {
527                 logger.fine("RFC2965CookieParser.domainMatch(): Host equals Domain. TRUE.");
528 
529                 return (true);
530             }
531 
532             if (Utils.isIPAddress(domain)) {
533                 logger.fine("RFC2965CookieParser.domainMatch(): Domain is IP.");
534 
535                 if (Utils.isIPAddress(host)) {
536                     logger.fine("RFC2965CookieParser.domainMatch(): Host is also IP." + host.equals(domain));
537 
538                     return (host.equals(domain));
539                 } else {
540                     logger.fine("RFC2965CookieParser.domainMatch(): Host is not IP.");
541 
542                     InetAddress ia = InetAddress.getByName(host);
543 
544                     logger.fine("RFC2965CookieParser.domainMatch(): Host IP=" + ia.getHostAddress());
545 
546                     return (domain.equals(ia.getHostAddress()));
547                 }
548             }
549 
550             if (domain.charAt(0) != '.') {
551                 logger.fine("RFC2965CookieParser.domainMatch(): Explicit domain doesn't have '.'.FALSE");
552 
553                 return (false);
554             }
555 
556             String bdash = domain.substring(1);
557 
558             if ((bdash.indexOf(".") != -1) || bdash.equalsIgnoreCase("local")) {
559                 return (host.toLowerCase().endsWith(bdash.toLowerCase()));
560             }
561         } catch (Exception e) {
562             logger.log(Level.SEVERE, "should not happen", e);
563         }
564 
565         return (false);
566     }
567 
568     /**
569 
570      * Performs "port matching" of URL to cookie portlist, for Version 1 cookies, as specified by RFC 2965. The given <code>URL</code>
571 
572      * port-matches the given portlist, if the port returned by <code>URL.getPort()</code> exists in the
573 
574      * portlist. The portlist itself is a comma-separated list of allowed ports for that cookie. If <code>URL.getPort()</code>
575 
576      * returns a value less than 0, the default port of 80 is assumed.
577 
578      *
579 
580      * @see URL
581 
582      * @param url the <code>URL</code> which must be matched.
583 
584      * @param portList the comma-separated list of acceptable ports.
585 
586      * @return <code>true</code> if the URL port exists in portList, or if portList is empty; <code>false</code> otherwise.
587 
588      */
589     public static final boolean portMatch(URL url, String portList) {
590         logger.fine("RFC2965CookieParser.portMatch(): URL=" + url + ",PortList=" + portList);
591 
592         int p = url.getPort();
593 
594         if (p < 0) {
595             p = 80;
596         }
597 
598         String port = String.valueOf(p);
599 
600         logger.fine("RFC2965CookieParser.portMatch(): Host port=" + port);
601 
602         if (!Utils.isNullOrWhiteSpace(portList)) {
603             StringTokenizer st = new StringTokenizer(portList, ",");
604 
605             while (st.hasMoreTokens()) {
606                 if (port.equals(st.nextToken().trim())) {
607                     logger.fine("RFC2965CookieParser.portMatch(): Match TRUE");
608 
609                     return (true);
610                 }
611             }
612 
613             logger.fine("RFC2965CookieParser.portMatch(): Match FALSE");
614 
615             return (false);
616         }
617 
618         logger.fine("RFC2965CookieParser.portMatch(): Match TRUE");
619 
620         return (true);
621     }
622 
623     /**
624 
625      * Converts a single Cookie to a form suitable for sending with a request, back to the server.
626 
627      * @param c the Cookie to be converted
628 
629      * @param bIncludeVersion whether the version number should be included
630 
631      * @return the String representing the formatted Cookie
632 
633      */
634     public static String toCookieHeaderForm(Cookie c, boolean bIncludeVersion) {
635         if ((c == null) || !c.isValid()) {
636             throw new IllegalArgumentException("Cookie is null OR cookie is invalid");
637         }
638 
639         StringBuffer sb = new StringBuffer();
640 
641         if ("0".equals(c.getVersion())) {
642             sb.append(c.getName());
643 
644             sb.append("=");
645 
646             sb.append(c.getValue());
647         } else {
648             if (bIncludeVersion) {
649                 sb.append("$Version=");
650 
651                 sb.append(c.getVersion());
652 
653                 sb.append(";");
654             }
655 
656             sb.append(c.getName());
657 
658             sb.append("=");
659 
660             sb.append("\"");
661 
662             sb.append(c.getValue());
663 
664             sb.append("\"");
665 
666             if (c.explicitPath()) {
667                 sb.append(";$Path=");
668 
669                 sb.append(c.getPath());
670             }
671 
672             if (c.explicitDomain()) {
673                 sb.append(";$Domain=");
674 
675                 sb.append(c.getDomain());
676             }
677 
678             if (c.explicitPort()) {
679                 sb.append(";$Port");
680 
681                 if (c.portListSpecified()) {
682                     sb.append("=\"");
683 
684                     sb.append(c.getPortList());
685 
686                     sb.append("\"");
687                 }
688             }
689         }
690 
691         return (sb.toString());
692     }
693 
694     private static boolean allowedCookie(Cookie c, URL url, boolean bStrict)
695         throws MalformedCookieException {
696         if ((c == null) || (url == null)) {
697             throw new IllegalArgumentException("Null cookie or URL");
698         }
699 
700         if (!c.isValid()) {
701             if (bStrict) {
702                 throw new MalformedCookieException("Invalid cookie", "SBCL_0012", RFC2965CookieParser.class,
703                     "allowedCookie");
704             }
705 
706             return (false);
707         }
708 
709         if ("1".equals(c.getVersion())) {
710             logger.fine("RFC2965CookieParser.allowedCookie(): Version 1 Cookie");
711 
712             if (domainMatch(url, c.getDomain())) {
713                 logger.fine("RFC2965CookieParser.allowedCookie(): Domain Matches.");
714 
715                 if (pathMatch(url, c.getPath())) {
716                     logger.fine("RFC2965CookieParser.allowedCookie(): Path Matches.");
717 
718                     if (portMatch(url, c.getPortList())) {
719                         logger.fine("RFC2965CookieParser.allowedCookie(): Port Matches.");
720 
721                         String host = url.getHost();
722 
723                         host = host.toLowerCase().trim();
724 
725                         if (host.indexOf('.') == -1) {
726                             host += ".local";
727                         }
728 
729                         String d = c.getDomain().toLowerCase().trim();
730 
731                         String h = host.substring(0, host.lastIndexOf(d));
732 
733                         logger.fine("RFC2965CookieParser.allowedCookie(): HOST=" + host + ",D=" + d + ",H=" + h);
734 
735                         if (countTheDots(h) > 0) {
736                             if (bStrict) {
737                                 throw new MalformedCookieException("X.Y.Z.COM tried to set domain=Y.COM", "SBCL_0018",
738                                     RFC2965CookieParser.class, "allowedCookie");
739                             }
740 
741                             return (false);
742                         }
743 
744                         logger.fine("RFC2965CookieParser.allowedCookie(): Security clearance granted.");
745 
746                         return (true);
747                     } else {
748                         if (bStrict) {
749                             throw new MalformedCookieException("PortMatch failed", "SBCL_0016",
750                                 RFC2965CookieParser.class, "allowedCookie");
751                         }
752                     }
753                 } else {
754                     if (bStrict) {
755                         throw new MalformedCookieException("PathMatch failed", "SBCL_0017", RFC2965CookieParser.class,
756                             "allowedCookie");
757                     }
758                 }
759             } else {
760                 if (bStrict) {
761                     throw new MalformedCookieException("DomainMatch failed", "SBCL_0015", RFC2965CookieParser.class,
762                         "allowedCookie");
763                 }
764             }
765         } else if ("0".equals(c.getVersion())) {
766             logger.fine("RFC2965CookieParser.allowedCookie(): Version 0 cookie");
767 
768             if (tailMatch(url, c.getDomain())) {
769                 return (true);
770             } else {
771                 if (bStrict) {
772                     throw new MalformedCookieException("TailMatch", "SBCL_0019", RFC2965CookieParser.class,
773                         "allowedCookie");
774                 }
775             }
776         } else {
777             if (bStrict) {
778                 throw new MalformedCookieException("Security Violated. Unknown reason.", "SBCL_0013",
779                     RFC2965CookieParser.class, "allowedCookie");
780             }
781         }
782 
783         logger.fine("RFC2965CookieParser.allowedCookie(): Security Violated. FALSE.");
784 
785         return (false);
786     }
787 
788     /**
789 
790      * Parses headers into Version 1 Cookies.
791 
792      */
793     public static final CookieJar parseSetCookieV1(Header responseHeader, URL url, boolean bStrict)
794         throws MalformedCookieException {
795         /*
796 
797         1.) WILL FAIL IF PORTLIST IS NOT QUOTED AS PER RFC2965
798 
799         2.) Must parse both "set-cookie" (RFC2109) and "set-cookie2" (RFC2965)
800 
801         */
802         if ((responseHeader == null) || responseHeader.isEmpty()) {
803             throw new IllegalArgumentException("No Headers");
804         }
805 
806         if (url == null) {
807             throw new IllegalArgumentException("Null source URL");
808         }
809 
810         if (!responseHeader.containsKey("set-cookie2") && !responseHeader.containsKey("set-cookie")) {
811             logger.fine("RFC2965CookieParser.parseSetCookieV1(): No valid headers.");
812 
813             return (null);
814         }
815 
816         String cookieToken = "";
817         String key;
818         String value;
819 
820         HeaderEntry he;
821 
822         CookieJar cj = new CookieJar();
823 
824         Cookie c;
825 
826         StringTokenizer st;
827 
828         Iterator it = responseHeader.iterator();
829 
830         while (it.hasNext()) {
831             he = (HeaderEntry) it.next();
832 
833             key = he.getKey();
834 
835             value = he.getValue();
836 
837             logger.fine("RFC2965CookieParser.parseSetCookieV1(): HEADERKEY=" + key);
838 
839             if (Utils.isNullOrWhiteSpace(key)) {
840                 continue;
841             }
842 
843             if (!(key.equalsIgnoreCase("set-cookie2") || key.equalsIgnoreCase("set-cookie"))) {
844                 continue;
845             }
846 
847             logger.fine("RFC2965CookieParser.parseSetCookieV1(): HEADERVALUE=" + value);
848 
849             if (!Utils.matchQuotes(value)) {
850                 if (bStrict) {
851                     throw new MalformedCookieException("Unmatched quotes throughout header.", "SBCL_0009",
852                         RFC2965CookieParser.class, "parseSetCookieV1");
853                 }
854             }
855 
856             st = new StringTokenizer(value, ",");
857 
858             while (st.hasMoreTokens()) {
859                 cookieToken += st.nextToken();
860 
861                 logger.fine("RFC2965CookieParser.parseSetCookieV1(): cookieToken=" + cookieToken);
862 
863                 if (!Utils.matchQuotes(cookieToken)) {
864                     logger.fine("RFC2965CookieParser.parseSetCookieV1(): Comma is inside quotes.");
865 
866                     cookieToken += ",";
867 
868                     continue;
869                 }
870 
871                 try {
872                     logger.fine("RFC2965CookieParser.parseSetCookieV1(): Parsing single cookie.");
873 
874                     c = parseSingleCookieV1(cookieToken, url, bStrict);
875 
876                     logger.fine("RFC2965CookieParser.parseSetCookieV1(): Parsed.COOKIE="
877                         + ((c == null) ? "null" : c.toString()));
878                 } catch (MalformedCookieException mce) {
879                     if (bStrict) {
880                         throw mce;
881                     }
882 
883                     c = null;
884                 }
885 
886                 if (c != null) {
887                     cj.add(c);
888                 }
889 
890                 cookieToken = "";
891             }
892         }
893 
894         logger.fine("RFC2965CookieParser.parseSetCookieV1(): Parsed all. COOKIEJAR=" + cj.toString());
895 
896         return (cj);
897     }
898 
899     /**
900 
901      * Parses headers into Version 1 Cookies.
902 
903      */
904     public static final CookieJar parseSetCookieV0(Header responseHeader, URL url, boolean bStrict)
905         throws MalformedCookieException {
906         if ((responseHeader == null) || responseHeader.isEmpty()) {
907             throw new IllegalArgumentException("No Headers");
908         }
909 
910         if (url == null) {
911             throw new IllegalArgumentException("Null source URL");
912         }
913 
914         if (!responseHeader.containsKey("set-cookie")) {
915             logger.fine("RFC2965CookieParser.parseSetCookieV0(): No Set-Cookie header.");
916 
917             return (null);
918         }
919 
920         String key;
921         String value;
922         String cookieToken = "";
923 
924         HeaderEntry he;
925 
926         CookieJar cj = new CookieJar();
927 
928         Cookie c;
929 
930         StringTokenizer st;
931 
932         Iterator it = responseHeader.iterator();
933 
934         while (it.hasNext()) {
935             he = (HeaderEntry) it.next();
936 
937             key = he.getKey();
938 
939             value = he.getValue();
940 
941             logger.fine("RFC2965CookieParser.parseSetCookieV0(): HEADERKEY: " + key);
942 
943             if (Utils.isNullOrWhiteSpace(key)) {
944                 continue;
945             }
946 
947             if (!key.equalsIgnoreCase("set-cookie")) {
948                 continue;
949             }
950 
951             logger.fine("RFC2965CookieParser.parseSetCookieV0(): HEADERVALUE: " + value);
952 
953             if (!Utils.matchQuotes(value)) {
954                 if (bStrict) {
955                     throw new MalformedCookieException("Unmatched quotes throughout header.", "SBCL_0009",
956                         RFC2965CookieParser.class, "parseSetCookieV0");
957                 }
958             }
959 
960             st = new StringTokenizer(value, ",");
961 
962             while (st.hasMoreTokens()) {
963                 cookieToken += st.nextToken();
964 
965                 logger.fine("RFC2965CookieParser.parseSetCookieV0(): cookieToken=" + cookieToken);
966 
967                 if (!Utils.matchQuotes(cookieToken)) {
968                     cookieToken += ",";
969 
970                     continue;
971                 }
972 
973                 // The expires value may have comma
974                 if (cookieToken.toLowerCase().indexOf("expires") != -1) {
975                     logger.fine("RFC2965CookieParser.parseSetCookieV0(): Found comma and expires.");
976 
977                     int eq = cookieToken.lastIndexOf("=");
978 
979                     if (eq != -1) {
980                         logger.fine("RFC2965CookieParser.parseSetCookieV0(): = sign exists.");
981 
982                         String last = cookieToken.substring(eq + 1);
983 
984                         logger.fine("RFC2965CookieParser.parseSetCookieV0(): Last word=" + last);
985 
986                         if (isWeekDay(Utils.trimWhitespace(last))) {
987                             logger.fine("RFC2965CookieParser.parseSetCookieV0(): False alarm.");
988 
989                             cookieToken += ",";
990 
991                             continue;
992                         }
993                     }
994                 }
995 
996                 try {
997                     c = parseSingleCookieV0(cookieToken, url, bStrict);
998 
999                     logger.fine("RFC2965CookieParser.parseSetCookieV0(): Parsed single cookie. C=" + c);
1000                } catch (MalformedCookieException mce) {
1001                    if (bStrict) {
1002                        throw mce;
1003                    }
1004
1005                    c = null;
1006                }
1007
1008                if (c != null) {
1009                    cj.add(c);
1010                }
1011
1012                cookieToken = "";
1013            }
1014        }
1015
1016        logger.fine("RFC2965CookieParser.parseSetCookieV0(): All processed. CJ=" + cj);
1017
1018        return (cj);
1019    }
1020
1021    private static final boolean isWeekDay(String str) {
1022        final String[] weekdays = {
1023            "sun", "sunday",
1024            
1025            "mon", "monday",
1026            
1027            "tue", "tuesday",
1028            
1029            "wed", "wednesday",
1030            
1031            "thu", "thursday",
1032            
1033            "fri", "friday",
1034            
1035            "sat", "saturday"
1036        };
1037
1038        if (Utils.isNullOrWhiteSpace(str)) {
1039            return (false);
1040        }
1041
1042        String s = str.trim().toLowerCase();
1043
1044        for (int i = 0; i < weekdays.length; i++) {
1045            if (s.equals(weekdays[i])) {
1046                return (true);
1047            }
1048        }
1049
1050        return (false);
1051    }
1052
1053    /**
1054
1055     * Converts a single cookie-string into a Cookie object, for Version 0
1056
1057     */
1058    public static Cookie parseSingleCookieV0(String s, URL url, boolean bStrict)
1059        throws MalformedCookieException {
1060        /*
1061
1062        If expires value can't be parsed into valid date, continues
1063
1064        quietly.
1065
1066        */
1067        if (Utils.isNullOrWhiteSpace(s) || isRFC2965CookieString(s)) {
1068            return (null);
1069        }
1070
1071        if (!Utils.matchQuotes(s)) {
1072            if (bStrict) {
1073                throw new MalformedCookieException("Unmatched quotes in cookie.", "SBCL_0010",
1074                    RFC2965CookieParser.class, "parseSingleCookieV0");
1075            }
1076        }
1077
1078        StringTokenizer st = new StringTokenizer(s, ";");
1079
1080        Cookie c = new Cookie();
1081
1082        String av = "";
1083        String attr;
1084        String val;
1085
1086        int i;
1087
1088        boolean bValisQuoted = false;
1089
1090        c.setDomain(url);
1091
1092        c.setPath(url);
1093
1094        c.setVersion("0");
1095
1096        while (st.hasMoreTokens()) {
1097            av += st.nextToken();
1098
1099            attr = "";
1100
1101            val = "";
1102
1103            bValisQuoted = false;
1104
1105            if (!Utils.matchQuotes(av)) {
1106                av += ";";
1107
1108                continue;
1109            }
1110
1111            if (Utils.isNullOrWhiteSpace(av)) {
1112                av = "";
1113
1114                continue;
1115            }
1116
1117            av = Utils.trimWhitespace(av);
1118
1119            i = av.indexOf('=');
1120
1121            if (i == -1) {
1122                i = av.length();
1123            }
1124
1125            attr = Utils.trimWhitespace(av.substring(0, i));
1126
1127            if (Utils.isNullOrWhiteSpace(attr)) {
1128                if (bStrict) {
1129                    throw new MalformedCookieException("Wierd cookie.", "SBCL_0002", RFC2965CookieParser.class,
1130                        "parseSingleCookieV0");
1131                }
1132
1133                av = "";
1134
1135                continue;
1136            }
1137
1138            if (i == av.length()) {
1139                val = "";
1140            } else {
1141                val = av.substring(i + 1);
1142            }
1143
1144            val = Utils.trimWhitespace(val);
1145
1146            bValisQuoted = Utils.isQuoted(val);
1147
1148            val = Utils.stripQuotes(val);
1149
1150            if (Utils.isEmpty(val)) {
1151                if (Utils.isNullOrWhiteSpace(c.getName()) && (i != av.length())) {
1152                    c.setName(attr);
1153
1154                    c.setValue(val);
1155                } else if ("secure".equalsIgnoreCase(attr)) {
1156                    c.setSecure(true);
1157                } else {
1158                    // Unrecognised attribute in AVPair with empty RHS
1159                    // Do what ?
1160                }
1161
1162                av = "";
1163
1164                continue;
1165            }
1166
1167            if ("domain".equalsIgnoreCase(attr)) {
1168                c.setDomain(val);
1169            } else if ("path".equalsIgnoreCase(attr)) {
1170                c.setPath(val);
1171            } else if ("expires".equalsIgnoreCase(attr)) {
1172                Date d = Utils.parseHttpDateStringToDate(val);
1173
1174                if ((d == null) && bStrict) {
1175                    throw new MalformedCookieException("Unparseable expires.", "SBCL_0011", RFC2965CookieParser.class,
1176                        "parseSingleCookieV0");
1177                }
1178
1179                c.setExpires(d);
1180            } else if (Utils.isNullOrWhiteSpace(c.getName())) {
1181                c.setName(attr);
1182
1183                c.setValue(bValisQuoted ? ("\"" + val + "\"") : val);
1184            } else {
1185                // Unrecognised attribute in AVPair.
1186                // Do nothing.
1187            }
1188
1189            av = "";
1190        }
1191
1192        if (s.toLowerCase().indexOf("expires") == -1) {
1193            c.setExpires(null);
1194        }
1195
1196        if (!c.isValid()) {
1197            if (bStrict) {
1198                throw new MalformedCookieException("Invalid cookie.", "SBCL_0012", RFC2965CookieParser.class,
1199                    "parseSingleCookieV0");
1200            }
1201        } else if (!allowedCookie(c, url, bStrict)) {
1202        } else {
1203            return (c);
1204        }
1205
1206        return (null);
1207    }
1208
1209    /**
1210
1211     * Converts a single cookie-string into a Cookie object, for Version 1
1212
1213     */
1214    public static Cookie parseSingleCookieV1(String s, URL url, boolean bStrict)
1215        throws MalformedCookieException {
1216        /*
1217
1218            This parser is both strict and lenient.
1219
1220            Lenient :-
1221
1222            1.) If commentURL is not valid URL (MalformedURLException),
1223
1224                it continues parsing
1225
1226            2.) If Max-Age is not valid number (NumberFormatException),
1227
1228                it continues parsing
1229
1230            3.) If PortList has invalid number (NFE), continues
1231
1232            Strict :-
1233
1234            1.) NAME-VALUE pair must be first AVPair
1235
1236            2.) Version must be set. If Cookie doesn't support that version
1237
1238                Exception is thrown up.
1239
1240            3.) The NAME must not begin with '$'
1241
1242            4.) The server must have permission to set cookie with given params.
1243
1244                (Domain,path,port)
1245
1246
1247
1248            NOTE:
1249
1250            Must add code to distinguish between V0 and V1 (RFC2109). This is done
1251
1252                using Version attr. If it is there, then V1, else V0
1253
1254        */
1255        logger.fine("RFC2965CookieParser.parseSingleCookieV1(): Parsing. S=" + s + ",URL=" + url);
1256
1257        if (Utils.isNullOrWhiteSpace(s) || !isRFC2965CookieString(s)) {
1258            return (null);
1259        }
1260
1261        if (!Utils.matchQuotes(s)) {
1262            if (bStrict) {
1263                throw new MalformedCookieException("Unmatched quotes in cookie.", "SBCL_0010",
1264                    RFC2965CookieParser.class, "parseSingleCookieV1");
1265            }
1266        }
1267
1268        logger.fine(s);
1269
1270        StringTokenizer st = new StringTokenizer(s, ";");
1271
1272        Cookie c = new Cookie();
1273
1274        String av = "";
1275        String attr;
1276        String val;
1277
1278        int i;
1279
1280        boolean bGotVersion = false;
1281
1282        c.setDomain(url);
1283
1284        c.setPath(url);
1285
1286        c.setVersion("1");
1287
1288        while (st.hasMoreTokens()) {
1289            av += st.nextToken();
1290
1291            attr = "";
1292
1293            val = "";
1294
1295            if (!Utils.matchQuotes(av)) {
1296                av += ";";
1297
1298                continue;
1299            }
1300
1301            if (Utils.isNullOrWhiteSpace(av)) {
1302                av = "";
1303
1304                continue;
1305            }
1306
1307            av = Utils.trimWhitespace(av);
1308
1309            i = av.indexOf('=');
1310
1311            if (i == -1) {
1312                if (Utils.isNullOrWhiteSpace(c.getName())) {
1313                    throw new MalformedCookieException("Non-conforming cookie.", "SBCL_0001",
1314                        RFC2965CookieParser.class, "parseSingleCookieV1");
1315                }
1316
1317                i = av.length();
1318            }
1319
1320            attr = Utils.trimWhitespace(av.substring(0, i));
1321
1322            if (Utils.isNullOrWhiteSpace(attr)) {
1323                if (bStrict) {
1324                    throw new MalformedCookieException("Wierd cookie.", "SBCL_0002", RFC2965CookieParser.class,
1325                        "parseSingleCookieV0");
1326                }
1327
1328                av = "";
1329
1330                continue;
1331            }
1332
1333            if (i == av.length()) {
1334                val = "";
1335            } else {
1336                val = av.substring(i + 1);
1337            }
1338
1339            val = Utils.stripQuotes(Utils.trimWhitespace(val));
1340
1341            if (Utils.isNullOrWhiteSpace(c.getName())) {
1342                if (attr.startsWith("$")) {
1343                    throw new MalformedCookieException("Non-conforming cookie.", "SBCL_0003",
1344                        RFC2965CookieParser.class, "parseSingleCookieV1");
1345                }
1346
1347                c.setName(attr);
1348
1349                c.setValue(val);
1350
1351                av = "";
1352
1353                continue;
1354            }
1355
1356            if (Utils.isEmpty(val)) {
1357                if ("port".equalsIgnoreCase(attr)) {
1358                    c.setPort(url);
1359                } else if ("secure".equalsIgnoreCase(attr)) {
1360                    c.setSecure(true);
1361                } else if ("discard".equalsIgnoreCase(attr)) {
1362                    c.setDiscard(true);
1363                } else {
1364                    // Unrecognised attribute in AVPair with empty RHS
1365                    // RFC2965 says should ignore, so do nothing.
1366                }
1367
1368                av = "";
1369
1370                continue;
1371            }
1372
1373            if ("comment".equalsIgnoreCase(attr)) {
1374                c.setComment(val);
1375            } else if ("commenturl".equalsIgnoreCase(attr)) {
1376                try {
1377                    c.setCommentURL(new URL(val));
1378                } catch (MalformedURLException mue) {
1379                    if (bStrict) {
1380                        throw new MalformedCookieException("Invalid data in Cookie.", mue, "SBCL_0004",
1381                            RFC2965CookieParser.class, "parseSingleCookieV1");
1382                    }
1383                }
1384            } else if ("domain".equalsIgnoreCase(attr)) {
1385                c.setDomain(val);
1386            } else if ("max-age".equalsIgnoreCase(attr)) {
1387                try {
1388                    c.setMaxAge(Integer.parseInt(val));
1389                } catch (NumberFormatException nfe) {
1390                    if (bStrict) {
1391                        throw new MalformedCookieException("Invalid data in Cookie.", nfe, "SBCL_0005",
1392                            RFC2965CookieParser.class, "parseSingleCookieV1");
1393                    }
1394                }
1395            } else if ("path".equalsIgnoreCase(attr)) {
1396                c.setPath(val);
1397            } else if ("port".equalsIgnoreCase(attr)) {
1398                try {
1399                    String[] strPorts = Utils.csvStringToArray(val);
1400
1401                    if (strPorts != null) {
1402                        int[] ports = new int[strPorts.length];
1403
1404                        for (int j = 0; j < strPorts.length; j++) {
1405                            ports[j] = Integer.parseInt(strPorts[j]);
1406                        }
1407
1408                        c.setPortList(ports);
1409                    }
1410                } catch (NumberFormatException nfe) {
1411                    if (bStrict) {
1412                        throw new MalformedCookieException("Invalid data in Cookie.", nfe, "SBCL_0006",
1413                            RFC2965CookieParser.class, "parseSingleCookieV1");
1414                    }
1415                }
1416            } else if ("version".equalsIgnoreCase(attr)) {
1417                c.setVersion(val);
1418
1419                bGotVersion = true;
1420            } else {
1421                // Unrecognised attribute in AVPair.
1422                // RFC2965 says should ignore, so do nothing.
1423            }
1424
1425            av = "";
1426        }
1427
1428        logger.fine("RFC2965CookieParser.parseSingleCookieV1(): Processing done. COOKIE=" + c.toString());
1429
1430        if (!c.isValid()) {
1431            if (bStrict) {
1432                throw new MalformedCookieException("Invalid cookie.", "SBCL_0012", RFC2965CookieParser.class,
1433