1 /*
2 * Copyright 2005 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26 package java.net;
27
28 import java.util.List;
29 import java.util.StringTokenizer;
30 import java.util.NoSuchElementException;
31 import java.text.SimpleDateFormat;
32 import java.util.TimeZone;
33 import java.util.Date;
34
35 import java.lang.NullPointerException; // for javadoc
36
37 /**
38 * An HttpCookie object represents an http cookie, which carries state
39 * information between server and user agent. Cookie is widely adopted
40 * to create stateful sessions.
41 *
42 * <p>There are 3 http cookie specifications:
43 * <blockquote>
44 * Netscape draft<br>
45 * RFC 2109 - <a href="http://www.ietf.org/rfc/rfc2109.txt">
46 * <i>http://www.ietf.org/rfc/rfc2109.txt</i></a><br>
47 * RFC 2965 - <a href="http://www.ietf.org/rfc/rfc2965.txt">
48 * <i>http://www.ietf.org/rfc/rfc2965.txt</i></a>
49 * </blockquote>
50 *
51 * <p>HttpCookie class can accept all these 3 forms of syntax.
52 *
53 * @author Edward Wang
54 * @since 1.6
55 */
56 public final class HttpCookie implements Cloneable {
57 /* ---------------- Fields -------------- */
58
59 //
60 // The value of the cookie itself.
61 //
62
63 private String name; // NAME= ... "$Name" style is reserved
64 private String value; // value of NAME
65
66 //
67 // Attributes encoded in the header's cookie fields.
68 //
69
70 private String comment; // Comment=VALUE ... describes cookie's use
71 private String commentURL; // CommentURL="http URL" ... describes cookie's use
72 private boolean toDiscard; // Discard ... discard cookie unconditionally
73 private String domain; // Domain=VALUE ... domain that sees cookie
74 private long maxAge = MAX_AGE_UNSPECIFIED; // Max-Age=VALUE ... cookies auto-expire
75 private String path; // Path=VALUE ... URLs that see the cookie
76 private String portlist; // Port[="portlist"] ... the port cookie may be returned to
77 private boolean secure; // Secure ... e.g. use SSL
78 private int version = 1; // Version=1 ... RFC 2965 style
79
80 //
81 // Hold the creation time (in seconds) of the http cookie for later
82 // expiration calculation
83 //
84 private long whenCreated = 0;
85
86
87 //
88 // Since the positive and zero max-age have their meanings,
89 // this value serves as a hint as 'not specify max-age'
90 //
91 private final static long MAX_AGE_UNSPECIFIED = -1;
92
93
94 //
95 // date formats used by Netscape's cookie draft
96 // as well as formats seen on various sites
97 //
98 private final static String[] COOKIE_DATE_FORMATS = {
99 "EEE',' dd-MMM-yyyy HH:mm:ss 'GMT'",
100 "EEE',' dd MMM yyyy HH:mm:ss 'GMT'",
101 "EEE MMM dd yyyy HH:mm:ss 'GMT'Z"
102 };
103
104 //
105 // constant strings represent set-cookie header token
106 //
107 private final static String SET_COOKIE = "set-cookie:";
108 private final static String SET_COOKIE2 = "set-cookie2:";
109
110
111 /* ---------------- Ctors -------------- */
112
113 /**
114 * Constructs a cookie with a specified name and value.
115 *
116 * <p>The name must conform to RFC 2965. That means it can contain
117 * only ASCII alphanumeric characters and cannot contain commas,
118 * semicolons, or white space or begin with a $ character. The cookie's
119 * name cannot be changed after creation.
120 *
121 * <p>The value can be anything the server chooses to send. Its
122 * value is probably of interest only to the server. The cookie's
123 * value can be changed after creation with the
124 * <code>setValue</code> method.
125 *
126 * <p>By default, cookies are created according to the RFC 2965
127 * cookie specification. The version can be changed with the
128 * <code>setVersion</code> method.
129 *
130 *
131 * @param name a <code>String</code> specifying the name of the cookie
132 *
133 * @param value a <code>String</code> specifying the value of the cookie
134 *
135 * @throws IllegalArgumentException if the cookie name contains illegal characters
136 * or it is one of the tokens reserved for use
137 * by the cookie protocol
138 * @throws NullPointerException if <tt>name</tt> is <tt>null</tt>
139 * @see #setValue
140 * @see #setVersion
141 *
142 */
143
144 public HttpCookie(String name, String value) {
145 name = name.trim();
146 if (name.length() == 0 || !isToken(name) || isReserved(name)) {
147 throw new IllegalArgumentException("Illegal cookie name");
148 }
149
150 this.name = name;
151 this.value = value;
152 toDiscard = false;
153 secure = false;
154
155 whenCreated = System.currentTimeMillis();
156 portlist = null;
157 }
158
159
160 /**
161 * Constructs cookies from set-cookie or set-cookie2 header string.
162 * RFC 2965 section 3.2.2 set-cookie2 syntax indicates that one header line
163 * may contain more than one cookie definitions, so this is a static
164 * utility method instead of another constructor.
165 *
166 * @param header a <tt>String</tt> specifying the set-cookie header.
167 * The header should start with "set-cookie", or "set-cookie2"
168 * token; or it should have no leading token at all.
169 * @return a List of cookie parsed from header line string
170 * @throws IllegalArgumentException if header string violates the cookie
171 * specification's syntax, or the cookie
172 * name contains llegal characters, or
173 * the cookie name is one of the tokens
174 * reserved for use by the cookie protocol
175 * @throws NullPointerException if the header string is <tt>null</tt>
176 */
177 public static List<HttpCookie> parse(String header) {
178 int version = guessCookieVersion(header);
179
180 // if header start with set-cookie or set-cookie2, strip it off
181 if (startsWithIgnoreCase(header, SET_COOKIE2)) {
182 header = header.substring(SET_COOKIE2.length());
183 } else if (startsWithIgnoreCase(header, SET_COOKIE)) {
184 header = header.substring(SET_COOKIE.length());
185 }
186
187
188 List<HttpCookie> cookies = new java.util.ArrayList<HttpCookie>();
189 // The Netscape cookie may have a comma in its expires attribute,
190 // while the comma is the delimiter in rfc 2965/2109 cookie header string.
191 // so the parse logic is slightly different
192 if (version == 0) {
193 // Netscape draft cookie
194 HttpCookie cookie = parseInternal(header);
195 cookie.setVersion(0);
196 cookies.add(cookie);
197 } else {
198 // rfc2965/2109 cookie
199 // if header string contains more than one cookie,
200 // it'll separate them with comma
201 List<String> cookieStrings = splitMultiCookies(header);
202 for (String cookieStr : cookieStrings) {
203 HttpCookie cookie = parseInternal(cookieStr);
204 cookie.setVersion(1);
205 cookies.add(cookie);
206 }
207 }
208
209 return cookies;
210 }
211
212
213
214
215 /* ---------------- Public operations -------------- */
216
217
218 /**
219 * Reports whether this http cookie has expired or not.
220 *
221 * @return <tt>true</tt> to indicate this http cookie has expired;
222 * otherwise, <tt>false</tt>
223 */
224 public boolean hasExpired() {
225 if (maxAge == 0) return true;
226
227 // if not specify max-age, this cookie should be
228 // discarded when user agent is to be closed, but
229 // it is not expired.
230 if (maxAge == MAX_AGE_UNSPECIFIED) return false;
231
232 long deltaSecond = (System.currentTimeMillis() - whenCreated) / 1000;
233 if (deltaSecond > maxAge)
234 return true;
235 else
236 return false;
237 }
238
239 /**
240 *
241 * Specifies a comment that describes a cookie's purpose.
242 * The comment is useful if the browser presents the cookie
243 * to the user. Comments
244 * are not supported by Netscape Version 0 cookies.
245 *
246 * @param purpose a <code>String</code> specifying the comment
247 * to display to the user
248 *
249 * @see #getComment
250 *
251 */
252
253 public void setComment(String purpose) {
254 comment = purpose;
255 }
256
257
258
259
260 /**
261 * Returns the comment describing the purpose of this cookie, or
262 * <code>null</code> if the cookie has no comment.
263 *
264 * @return a <code>String</code> containing the comment,
265 * or <code>null</code> if none
266 *
267 * @see #setComment
268 *
269 */
270
271 public String getComment() {
272 return comment;
273 }
274
275
276 /**
277 *
278 * Specifies a comment url that describes a cookie's purpose.
279 * The comment url is useful if the browser presents the cookie
280 * to the user. Comment url is RFC 2965 only.
281 *
282 * @param purpose a <code>String</code> specifying the comment url
283 * to display to the user
284 *
285 * @see #getCommentURL
286 *
287 */
288
289 public void setCommentURL(String purpose) {
290 commentURL = purpose;
291 }
292
293
294
295
296 /**
297 * Returns the comment url describing the purpose of this cookie, or
298 * <code>null</code> if the cookie has no comment url.
299 *
300 * @return a <code>String</code> containing the comment url,
301 * or <code>null</code> if none
302 *
303 * @see #setCommentURL
304 *
305 */
306
307 public String getCommentURL() {
308 return commentURL;
309 }
310
311
312 /**
313 * Specify whether user agent should discard the cookie unconditionally.
314 * This is RFC 2965 only attribute.
315 *
316 * @param discard <tt>true</tt> indicates to discard cookie unconditionally
317 *
318 * @see #getDiscard
319 */
320
321 public void setDiscard(boolean discard) {
322 toDiscard = discard;
323 }
324
325
326
327
328 /**
329 * Return the discard attribute of the cookie
330 *
331 * @return a <tt>boolean</tt> to represent this cookie's discard attribute
332 *
333 * @see #setDiscard
334 */
335
336 public boolean getDiscard() {
337 return toDiscard;
338 }
339
340
341 /**
342 * Specify the portlist of the cookie, which restricts the port(s)
343 * to which a cookie may be sent back in a Cookie header.
344 *
345 * @param ports a <tt>String</tt> specify the port list, which is
346 * comma seperated series of digits
347 * @see #getPortlist
348 */
349
350 public void setPortlist(String ports) {
351 portlist = ports;
352 }
353
354
355
356
357 /**
358 * Return the port list attribute of the cookie
359 *
360 * @return a <tt>String</tt> contains the port list
361 * or <tt>null</tt> if none
362 * @see #setPortlist
363 */
364
365 public String getPortlist() {
366 return portlist;
367 }
368
369 /**
370 *
371 * Specifies the domain within which this cookie should be presented.
372 *
373 * <p>The form of the domain name is specified by RFC 2965. A domain
374 * name begins with a dot (<code>.foo.com</code>) and means that
375 * the cookie is visible to servers in a specified Domain Name System
376 * (DNS) zone (for example, <code>www.foo.com</code>, but not
377 * <code>a.b.foo.com</code>). By default, cookies are only returned
378 * to the server that sent them.
379 *
380 *
381 * @param pattern a <code>String</code> containing the domain name
382 * within which this cookie is visible;
383 * form is according to RFC 2965
384 *
385 * @see #getDomain
386 *
387 */
388
389 public void setDomain(String pattern) {
390 if (pattern != null)
391 domain = pattern.toLowerCase();
392 else
393 domain = pattern;
394 }
395
396
397
398
399
400 /**
401 * Returns the domain name set for this cookie. The form of
402 * the domain name is set by RFC 2965.
403 *
404 * @return a <code>String</code> containing the domain name
405 *
406 * @see #setDomain
407 *
408 */
409
410 public String getDomain() {
411 return domain;
412 }
413
414
415 /**
416 * Sets the maximum age of the cookie in seconds.
417 *
418 * <p>A positive value indicates that the cookie will expire
419 * after that many seconds have passed. Note that the value is
420 * the <i>maximum</i> age when the cookie will expire, not the cookie's
421 * current age.
422 *
423 * <p>A negative value means
424 * that the cookie is not stored persistently and will be deleted
425 * when the Web browser exits. A zero value causes the cookie
426 * to be deleted.
427 *
428 * @param expiry an integer specifying the maximum age of the
429 * cookie in seconds; if zero, the cookie
430 * should be discarded immediately;
431 * otherwise, the cookie's max age is unspecified.
432 *
433 * @see #getMaxAge
434 *
435 */
436 public void setMaxAge(long expiry) {
437 maxAge = expiry;
438 }
439
440
441
442
443 /**
444 * Returns the maximum age of the cookie, specified in seconds.
445 * By default, <code>-1</code> indicating the cookie will persist
446 * until browser shutdown.
447 *
448 *
449 * @return an integer specifying the maximum age of the
450 * cookie in seconds
451 *
452 *
453 * @see #setMaxAge
454 *
455 */
456
457 public long getMaxAge() {
458 return maxAge;
459 }
460
461
462
463
464 /**
465 * Specifies a path for the cookie
466 * to which the client should return the cookie.
467 *
468 * <p>The cookie is visible to all the pages in the directory
469 * you specify, and all the pages in that directory's subdirectories.
470 * A cookie's path must include the servlet that set the cookie,
471 * for example, <i>/catalog</i>, which makes the cookie
472 * visible to all directories on the server under <i>/catalog</i>.
473 *
474 * <p>Consult RFC 2965 (available on the Internet) for more
475 * information on setting path names for cookies.
476 *
477 *
478 * @param uri a <code>String</code> specifying a path
479 *
480 *
481 * @see #getPath
482 *
483 */
484
485 public void setPath(String uri) {
486 path = uri;
487 }
488
489
490
491
492 /**
493 * Returns the path on the server
494 * to which the browser returns this cookie. The
495 * cookie is visible to all subpaths on the server.
496 *
497 *
498 * @return a <code>String</code> specifying a path that contains
499 * a servlet name, for example, <i>/catalog</i>
500 *
501 * @see #setPath
502 *
503 */
504
505 public String getPath() {
506 return path;
507 }
508
509
510
511
512
513 /**
514 * Indicates whether the cookie should only be sent using a secure protocol,
515 * such as HTTPS or SSL.
516 *
517 * <p>The default value is <code>false</code>.
518 *
519 * @param flag If <code>true</code>, the cookie can only be sent over
520 * a secure protocol like https.
521 * If <code>false</code>, it can be sent over any protocol.
522 *
523 * @see #getSecure
524 *
525 */
526
527 public void setSecure(boolean flag) {
528 secure = flag;
529 }
530
531
532
533
534 /**
535 * Returns <code>true</code> if sending this cookie should be
536 * restricted to a secure protocol, or <code>false</code> if the
537 * it can be sent using any protocol.
538 *
539 * @return <code>false</code> if the cookie can be sent over
540 * any standard protocol; otherwise, <code>true</code>
541 *
542 * @see #setSecure
543 *
544 */
545
546 public boolean getSecure() {
547 return secure;
548 }
549
550
551
552
553
554 /**
555 * Returns the name of the cookie. The name cannot be changed after
556 * creation.
557 *
558 * @return a <code>String</code> specifying the cookie's name
559 *
560 */
561
562 public String getName() {
563 return name;
564 }
565
566
567
568
569
570 /**
571 *
572 * Assigns a new value to a cookie after the cookie is created.
573 * If you use a binary value, you may want to use BASE64 encoding.
574 *
575 * <p>With Version 0 cookies, values should not contain white
576 * space, brackets, parentheses, equals signs, commas,
577 * double quotes, slashes, question marks, at signs, colons,
578 * and semicolons. Empty values may not behave the same way
579 * on all browsers.
580 *
581 * @param newValue a <code>String</code> specifying the new value
582 *
583 *
584 * @see #getValue
585 *
586 */
587
588 public void setValue(String newValue) {
589 value = newValue;
590 }
591
592
593
594
595 /**
596 * Returns the value of the cookie.
597 *
598 * @return a <code>String</code> containing the cookie's
599 * present value
600 *
601 * @see #setValue
602 *
603 */
604
605 public String getValue() {
606 return value;
607 }
608
609
610
611
612 /**
613 * Returns the version of the protocol this cookie complies
614 * with. Version 1 complies with RFC 2965/2109,
615 * and version 0 complies with the original
616 * cookie specification drafted by Netscape. Cookies provided
617 * by a browser use and identify the browser's cookie version.
618 *
619 *
620 * @return 0 if the cookie complies with the
621 * original Netscape specification; 1
622 * if the cookie complies with RFC 2965/2109
623 *
624 * @see #setVersion
625 *
626 */
627
628 public int getVersion() {
629 return version;
630 }
631
632
633
634
635 /**
636 * Sets the version of the cookie protocol this cookie complies
637 * with. Version 0 complies with the original Netscape cookie
638 * specification. Version 1 complies with RFC 2965/2109.
639 *
640 *
641 * @param v 0 if the cookie should comply with
642 * the original Netscape specification;
643 * 1 if the cookie should comply with RFC 2965/2109
644 *
645 * @throws IllegalArgumentException if <tt>v</tt> is neither 0 nor 1
646 *
647 * @see #getVersion
648 *
649 */
650
651 public void setVersion(int v) {
652 if (v != 0 && v != 1) {
653 throw new IllegalArgumentException("cookie version should be 0 or 1");
654 }
655
656 version = v;
657 }
658
659
660 /**
661 * The utility method to check whether a host name is in a domain
662 * or not.
663 *
664 * <p>This concept is described in the cookie specification.
665 * To understand the concept, some terminologies need to be defined first:
666 * <blockquote>
667 * effective host name = hostname if host name contains dot<br>
668 * or = hostname.local if not
669 * </blockquote>
670 * <p>Host A's name domain-matches host B's if:
671 * <blockquote><ul>
672 * <li>their host name strings string-compare equal; or</li>
673 * <li>A is a HDN string and has the form NB, where N is a non-empty
674 * name string, B has the form .B', and B' is a HDN string. (So,
675 * x.y.com domain-matches .Y.com but not Y.com.)</li>
676 * </ul></blockquote>
677 *
678 * <p>A host isn't in a domain (RFC 2965 sec. 3.3.2) if:
679 * <blockquote><ul>
680 * <li>The value for the Domain attribute contains no embedded dots,
681 * and the value is not .local.</li>
682 * <li>The effective host name that derives from the request-host does
683 * not domain-match the Domain attribute.</li>
684 * <li>The request-host is a HDN (not IP address) and has the form HD,
685 * where D is the value of the Domain attribute, and H is a string
686 * that contains one or more dots.</li>
687 * </ul></blockquote>
688 *
689 * <p>Examples:
690 * <blockquote><ul>
691 * <li>A Set-Cookie2 from request-host y.x.foo.com for Domain=.foo.com
692 * would be rejected, because H is y.x and contains a dot.</li>
693 * <li>A Set-Cookie2 from request-host x.foo.com for Domain=.foo.com
694 * would be accepted.</li>
695 * <li>A Set-Cookie2 with Domain=.com or Domain=.com., will always be
696 * rejected, because there is no embedded dot.</li>
697 * <li>A Set-Cookie2 with Domain=ajax.com will be accepted, and the
698 * value for Domain will be taken to be .ajax.com, because a dot
699 * gets prepended to the value.</li>
700 * <li>A Set-Cookie2 from request-host example for Domain=.local will
701 * be accepted, because the effective host name for the request-
702 * host is example.local, and example.local domain-matches .local.</li>
703 * </ul></blockquote>
704 *
705 * @param domain the domain name to check host name with
706 * @param host the host name in question
707 * @return <tt>true</tt> if they domain-matches; <tt>false</tt> if not
708 */
709 public static boolean domainMatches(String domain, String host) {
710 if (domain == null || host == null)
711 return false;
712
713 // if there's no embedded dot in domain and domain is not .local
714 boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
715 int embeddedDotInDomain = domain.indexOf('.');
716 if (embeddedDotInDomain == 0)
717 embeddedDotInDomain = domain.indexOf('.', 1);
718 if (!isLocalDomain
719 && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1))
720 return false;
721
722 // if the host name contains no dot and the domain name is .local
723 int firstDotInHost = host.indexOf('.');
724 if (firstDotInHost == -1 && isLocalDomain)
725 return true;
726
727 int domainLength = domain.length();
728 int lengthDiff = host.length() - domainLength;
729 if (lengthDiff == 0) {
730 // if the host name and the domain name are just string-compare euqal
731 return host.equalsIgnoreCase(domain);
732 }
733 else if (lengthDiff > 0) {
734 // need to check H & D component
735 String H = host.substring(0, lengthDiff);
736 String D = host.substring(lengthDiff);
737
738 return (H.indexOf('.') == -1 && D.equalsIgnoreCase(domain));
739 }
740 else if (lengthDiff == -1) {
741 // if domain is actually .host
742 return (domain.charAt(0) == '.' &&
743 host.equalsIgnoreCase(domain.substring(1)));
744 }
745
746 return false;
747 }
748
749
750 /**
751 * Constructs a cookie header string representation of this cookie,
752 * which is in the format defined by corresponding cookie specification,
753 * but without the leading "Cookie:" token.
754 *
755 * @return a string form of the cookie. The string has the defined format
756 */
757 @Override
758 public String toString() {
759 if (getVersion() > 0) {
760 return toRFC2965HeaderString();
761 } else {
762 return toNetscapeHeaderString();
763 }
764 }
765
766
767 /**
768 * Test the equality of two http cookies.
769 *
770 * <p> The result is <tt>true</tt> only if two cookies
771 * come from same domain (case-insensitive),
772 * have same name (case-insensitive),
773 * and have same path (case-sensitive).
774 *
775 * @return <tt>true</tt> if 2 http cookies equal to each other;
776 * otherwise, <tt>false</tt>
777 */
778 @Override
779 public boolean equals(Object obj) {
780 if (obj == this)
781 return true;
782 if (!(obj instanceof HttpCookie))
783 return false;
784 HttpCookie other = (HttpCookie)obj;
785
786 // One http cookie equals to another cookie (RFC 2965 sec. 3.3.3) if:
787 // 1. they come from same domain (case-insensitive),
788 // 2. have same name (case-insensitive),
789 // 3. and have same path (case-sensitive).
790 return equalsIgnoreCase(getName(), other.getName()) &&
791 equalsIgnoreCase(getDomain(), other.getDomain()) &&
792 equals(getPath(), other.getPath());
793 }
794
795
796 /**
797 * Return hash code of this http cookie. The result is the sum of
798 * hash code value of three significant components of this cookie:
799 * name, domain, and path.
800 * That is, the hash code is the value of the expression:
801 * <blockquote>
802 * getName().toLowerCase().hashCode()<br>
803 * + getDomain().toLowerCase().hashCode()<br>
804 * + getPath().hashCode()
805 * </blockquote>
806 *
807 * @return this http cookie's hash code
808 */
809 @Override
810 public int hashCode() {
811 int h1 = name.toLowerCase().hashCode();
812 int h2 = (domain!=null) ? domain.toLowerCase().hashCode() : 0;
813 int h3 = (path!=null) ? path.hashCode() : 0;
814
815 return h1 + h2 + h3;
816 }
817
818 /**
819 * Create and return a copy of this object.
820 *
821 * @return a clone of this http cookie
822 */
823 @Override
824 public Object clone() {
825 try {
826 return super.clone();
827 } catch (CloneNotSupportedException e) {
828 throw new RuntimeException(e.getMessage());
829 }
830 }
831
832
833 /* ---------------- Private operations -------------- */
834
835 // Note -- disabled for now to allow full Netscape compatibility
836 // from RFC 2068, token special case characters
837 //
838 // private static final String tspecials = "()<>@,;:\\\"/[]?={} \t";
839 private static final String tspecials = ",;";
840
841 /*
842 * Tests a string and returns true if the string counts as a
843 * token.
844 *
845 * @param value the <code>String</code> to be tested
846 *
847 * @return <code>true</code> if the <code>String</code> is
848 * a token; <code>false</code> if it is not
849 */
850
851 private static boolean isToken(String value) {
852 int len = value.length();
853
854 for (int i = 0; i < len; i++) {
855 char c = value.charAt(i);
856
857 if (c < 0x20 || c >= 0x7f || tspecials.indexOf(c) != -1)
858 return false;
859 }
860 return true;
861 }
862
863
864 /*
865 * @param name the name to be tested
866 * @return <tt>true</tt> if the name is reserved by cookie
867 * specification, <tt>false</tt> if it is not
868 */
869 private static boolean isReserved(String name) {
870 if (name.equalsIgnoreCase("Comment")
871 || name.equalsIgnoreCase("CommentURL") // rfc2965 only
872 || name.equalsIgnoreCase("Discard") // rfc2965 only
873 || name.equalsIgnoreCase("Domain")
874 || name.equalsIgnoreCase("Expires") // netscape draft only
875 || name.equalsIgnoreCase("Max-Age")
876 || name.equalsIgnoreCase("Path")
877 || name.equalsIgnoreCase("Port") // rfc2965 only
878 || name.equalsIgnoreCase("Secure")
879 || name.equalsIgnoreCase("Version")
880 || name.charAt(0) == '$')
881 {
882 return true;
883 }
884
885 return false;
886 }
887
888
889 /*
890 * Parse header string to cookie object.
891 *
892 * @param header header string; should contain only one NAME=VALUE pair
893 *
894 * @return an HttpCookie being extracted
895 *
896 * @throws IllegalArgumentException if header string violates the cookie
897 * specification
898 */
899 private static HttpCookie parseInternal(String header)
900 {
901 HttpCookie cookie = null;
902 String namevaluePair = null;
903
904 StringTokenizer tokenizer = new StringTokenizer(header, ";");
905
906 // there should always have at least on name-value pair;
907 // it's cookie's name
908 try {
909 namevaluePair = tokenizer.nextToken();
910 int index = namevaluePair.indexOf('=');
911 if (index != -1) {
912 String name = namevaluePair.substring(0, index).trim();
913 String value = namevaluePair.substring(index + 1).trim();
914 cookie = new HttpCookie(name, stripOffSurroundingQuote(value));
915 } else {
916 // no "=" in name-value pair; it's an error
917 throw new IllegalArgumentException("Invalid cookie name-value pair");
918 }
919 } catch (NoSuchElementException ignored) {
920 throw new IllegalArgumentException("Empty cookie header string");
921 }
922
923 // remaining name-value pairs are cookie's attributes
924 while (tokenizer.hasMoreTokens()) {
925 namevaluePair = tokenizer.nextToken();
926 int index = namevaluePair.indexOf('=');
927 String name, value;
928 if (index != -1) {
929 name = namevaluePair.substring(0, index).trim();
930 value = namevaluePair.substring(index + 1).trim();
931 } else {
932 name = namevaluePair.trim();
933 value = null;
934 }
935
936 // assign attribute to cookie
937 assignAttribute(cookie, name, value);
938 }
939
940 return cookie;
941 }
942
943
944 /*
945 * assign cookie attribute value to attribute name;
946 * use a map to simulate method dispatch
947 */
948 static interface CookieAttributeAssignor {
949 public void assign(HttpCookie cookie, String attrName, String attrValue);
950 }
951 static java.util.Map<String, CookieAttributeAssignor> assignors = null;
952 static {
953 assignors = new java.util.HashMap<String, CookieAttributeAssignor>();
954 assignors.put("comment", new CookieAttributeAssignor(){
955 public void assign(HttpCookie cookie, String attrName, String attrValue) {
956 if (cookie.getComment() == null) cookie.setComment(attrValue);
957 }
958 });
959 assignors.put("commenturl", new CookieAttributeAssignor(){
960 public void assign(HttpCookie cookie, String attrName, String attrValue) {
961 if (cookie.getCommentURL() == null) cookie.setCommentURL(attrValue);
962 }
963 });
964 assignors.put("discard", new CookieAttributeAssignor(){
965 public void assign(HttpCookie cookie, String attrName, String attrValue) {
966 cookie.setDiscard(true);
967 }
968 });
969 assignors.put("domain", new CookieAttributeAssignor(){
970 public void assign(HttpCookie cookie, String attrName, String attrValue) {
971 if (cookie.getDomain() == null) cookie.setDomain(attrValue);
972 }
973 });
974 assignors.put("max-age", new CookieAttributeAssignor(){
975 public void assign(HttpCookie cookie, String attrName, String attrValue) {
976 try {
977 long maxage = Long.parseLong(attrValue);
978 if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) cookie.setMaxAge(maxage);
979 } catch (NumberFormatException ignored) {
980 throw new IllegalArgumentException("Illegal cookie max-age attribute");
981 }
982 }
983 });
984 assignors.put("path", new CookieAttributeAssignor(){
985 public void assign(HttpCookie cookie, String attrName, String attrValue) {
986 if (cookie.getPath() == null) cookie.setPath(attrValue);
987 }
988 });
989 assignors.put("port", new CookieAttributeAssignor(){
990 public void assign(HttpCookie cookie, String attrName, String attrValue) {
991 if (cookie.getPortlist() == null) cookie.setPortlist(attrValue == null ? "" : attrValue);
992 }
993 });
994 assignors.put("secure", new CookieAttributeAssignor(){
995 public void assign(HttpCookie cookie, String attrName, String attrValue) {
996 cookie.setSecure(true);
997 }
998 });
999 assignors.put("version", new CookieAttributeAssignor(){
1000 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1001 try {
1002 int version = Integer.parseInt(attrValue);
1003 cookie.setVersion(version);
1004 } catch (NumberFormatException ignored) {
1005 throw new IllegalArgumentException("Illegal cookie version attribute");
1006 }
1007 }
1008 });
1009 assignors.put("expires", new CookieAttributeAssignor(){ // Netscape only
1010 public void assign(HttpCookie cookie, String attrName, String attrValue) {
1011 if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) {
1012 cookie.setMaxAge(cookie.expiryDate2DeltaSeconds(attrValue));
1013 }
1014 }
1015 });
1016 }
1017 private static void assignAttribute(HttpCookie cookie,
1018 String attrName,
1019 String attrValue)
1020 {
1021 // strip off the surrounding "-sign if there's any
1022 attrValue = stripOffSurroundingQuote(attrValue);
1023
1024 CookieAttributeAssignor assignor = assignors.get(attrName.toLowerCase());
1025 if (assignor != null) {
1026 assignor.assign(cookie, attrName, attrValue);
1027 } else {
1028 // must be an error
1029 throw new IllegalArgumentException("Illegal cookie attribute");
1030 }
1031 }
1032
1033 /*
1034 * Constructs a string representation of this cookie. The string format is
1035 * as Netscape spec, but without leading "Cookie:" token.
1036 */
1037 private String toNetscapeHeaderString() {
1038 StringBuilder sb = new StringBuilder();
1039
1040 sb.append(getName() + "=" + getValue());
1041
1042 return sb.toString();
1043 }
1044
1045 /*
1046 * Constructs a string representation of this cookie. The string format is
1047 * as RFC 2965/2109, but without leading "Cookie:" token.
1048 */
1049 private String toRFC2965HeaderString() {
1050 StringBuilder sb = new StringBuilder();
1051
1052 sb.append(getName()).append("=\"").append(getValue()).append('"');
1053 if (getPath() != null)
1054 sb.append(";$Path=\"").append(getPath()).append('"');
1055 if (getDomain() != null)
1056 sb.append(";$Domain=\"").append(getDomain()).append('"');
1057 if (getPortlist() != null)
1058 sb.append(";$Port=\"").append(getPortlist()).append('"');
1059
1060 return sb.toString();
1061 }
1062
1063 private static SimpleDateFormat[] cDateFormats = null;
1064 static {
1065 cDateFormats = new SimpleDateFormat[COOKIE_DATE_FORMATS.length];
1066 for (int i = 0; i < COOKIE_DATE_FORMATS.length; i++) {
1067 cDateFormats[i] = new SimpleDateFormat(COOKIE_DATE_FORMATS[i]);
1068 cDateFormats[i].setTimeZone(TimeZone.getTimeZone("GMT"));
1069 }
1070 }
1071 /*
1072 * @param dateString a date string in one of the formats
1073 * defined in Netscape cookie spec
1074 *
1075 * @return delta seconds between this cookie's creation
1076 * time and the time specified by dateString
1077 */
1078 private long expiryDate2DeltaSeconds(String dateString) {
1079 for (SimpleDateFormat df : cDateFormats) {
1080 try {
1081 Date date = df.parse(dateString);
1082 return (date.getTime() - whenCreated) / 1000;
1083 } catch (Exception e) {
1084
1085 }
1086 }
1087 return 0;
1088 }
1089
1090
1091
1092 /*
1093 * try to guess the cookie version through set-cookie header string
1094 */
1095 private static int guessCookieVersion(String header) {
1096 int version = 0;
1097
1098 header = header.toLowerCase();
1099 if (header.indexOf("expires=") != -1) {
1100 // only netscape cookie using 'expires'
1101 version = 0;
1102 } else if (header.indexOf("version=") != -1) {
1103 // version is mandatory for rfc 2965/2109 cookie
1104 version = 1;
1105 } else if (header.indexOf("max-age") != -1) {
1106 // rfc 2965/2109 use 'max-age'
1107 version = 1;
1108 } else if (startsWithIgnoreCase(header, SET_COOKIE2)) {
1109 // only rfc 2965 cookie starts with 'set-cookie2'
1110 version = 1;
1111 }
1112
1113 return version;
1114 }
1115
1116 private static String stripOffSurroundingQuote(String str) {
1117 if (str != null && str.length() > 0 &&
1118 str.charAt(0) == '"' && str.charAt(str.length() - 1) == '"') {
1119 return str.substring(1, str.length() - 1);
1120 } else {
1121 return str;
1122 }
1123 }
1124
1125 private static boolean equalsIgnoreCase(String s, String t) {
1126 if (s == t) return true;
1127 if ((s != null) && (t != null)) {
1128 return s.equalsIgnoreCase(t);
1129 }
1130 return false;
1131 }
1132
1133 private static boolean equals(String s, String t) {
1134 if (s == t) return true;
1135 if ((s != null) && (t != null)) {
1136 return s.equals(t);
1137 }
1138 return false;
1139 }
1140
1141 private static boolean startsWithIgnoreCase(String s, String start) {
1142 if (s == null || start == null) return false;
1143
1144 if (s.length() >= start.length() &&
1145 start.equalsIgnoreCase(s.substring(0, start.length()))) {
1146 return true;
1147 }
1148
1149 return false;
1150 }
1151
1152 /*
1153 * Split cookie header string according to rfc 2965:
1154 * 1) split where it is a comma;
1155 * 2) but not the comma surrounding by double-quotes, which is the comma
1156 * inside port list or embeded URIs.
1157 *
1158 * @param header the cookie header string to split
1159 *
1160 * @return list of strings; never null
1161 *
1162 */
1163 private static List<String> splitMultiCookies(String header) {
1164 List<String> cookies = new java.util.ArrayList<String>();
1165 int quoteCount = 0;
1166 int p, q;
1167
1168 for (p = 0, q = 0; p < header.length(); p++) {
1169 char c = header.charAt(p);
1170 if (c == '"') quoteCount++;
1171 if (c == ',' && (quoteCount % 2 == 0)) { // it is comma and not surrounding by double-quotes
1172 cookies.add(header.substring(q, p));
1173 q = p + 1;
1174 }
1175 }
1176
1177 cookies.add(header.substring(q));
1178
1179 return cookies;
1180 }
1181 }