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 * 1. By default (no Port attribute), the cookie MAY be sent to
213
214 * any port.<br>
215
216 * 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 * 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