1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 package org.apache.commons.httpclient.cookie;
31
32 import java.util.Collection;
33 import java.util.Date;
34 import java.util.Iterator;
35 import java.util.LinkedList;
36 import java.util.List;
37 import java.util.SortedMap;
38
39 import org.apache.commons.httpclient.Cookie;
40 import org.apache.commons.httpclient.Header;
41 import org.apache.commons.httpclient.HeaderElement;
42 import org.apache.commons.httpclient.NameValuePair;
43 import org.apache.commons.httpclient.util.DateParseException;
44 import org.apache.commons.httpclient.util.DateUtil;
45 import org.apache.commons.logging.Log;
46 import org.apache.commons.logging.LogFactory;
47
48 import com.google.common.net.InternetDomainName;
49 import com.sleepycat.collections.StoredIterator;
50
51 /***
52 *
53 * Cookie management functions shared by all specification.
54 *
55 * @author B.C. Holmes
56 * @author <a href="mailto:jericho@thinkfree.com">Park, Sung-Gu</a>
57 * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a>
58 * @author Rod Waldhoff
59 * @author dIon Gillard
60 * @author Sean C. Sullivan
61 * @author <a href="mailto:JEvans@Cyveillance.com">John Evans</a>
62 * @author Marc A. Saegesser
63 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
64 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
65 *
66 * @since 2.0
67 */
68 @SuppressWarnings("unchecked")
69 public class CookieSpecBase implements CookieSpec {
70
71 /*** Log object */
72 protected static final Log LOG = LogFactory.getLog(CookieSpec.class);
73
74 /*** Valid date patterns */
75 private Collection datepatterns = null;
76
77 /*** Default constructor */
78 public CookieSpecBase() {
79 super();
80 }
81
82
83 /***
84 * Parses the Set-Cookie value into an array of <tt>Cookie</tt>s.
85 *
86 * <P>The syntax for the Set-Cookie response header is:
87 *
88 * <PRE>
89 * set-cookie = "Set-Cookie:" cookies
90 * cookies = 1#cookie
91 * cookie = NAME "=" VALUE * (";" cookie-av)
92 * NAME = attr
93 * VALUE = value
94 * cookie-av = "Comment" "=" value
95 * | "Domain" "=" value
96 * | "Max-Age" "=" value
97 * | "Path" "=" value
98 * | "Secure"
99 * | "Version" "=" 1*DIGIT
100 * </PRE>
101 *
102 * @param host the host from which the <tt>Set-Cookie</tt> value was
103 * received
104 * @param port the port from which the <tt>Set-Cookie</tt> value was
105 * received
106 * @param path the path from which the <tt>Set-Cookie</tt> value was
107 * received
108 * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> value was
109 * received over secure conection
110 * @param header the <tt>Set-Cookie</tt> received from the server
111 * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie value
112 * @throws MalformedCookieException if an exception occurs during parsing
113 */
114 public Cookie[] parse(String host, int port, String path,
115 boolean secure, final String header)
116 throws MalformedCookieException {
117
118 LOG.trace("enter CookieSpecBase.parse("
119 + "String, port, path, boolean, Header)");
120
121 if (host == null) {
122 throw new IllegalArgumentException(
123 "Host of origin may not be null");
124 }
125 if (host.trim().equals("")) {
126 throw new IllegalArgumentException(
127 "Host of origin may not be blank");
128 }
129 if (port < 0) {
130 throw new IllegalArgumentException("Invalid port: " + port);
131 }
132 if (path == null) {
133 throw new IllegalArgumentException(
134 "Path of origin may not be null.");
135 }
136 if (header == null) {
137 throw new IllegalArgumentException("Header may not be null.");
138 }
139
140 if (path.trim().equals("")) {
141 path = PATH_DELIM;
142 }
143 host = host.toLowerCase();
144
145 String defaultPath = path;
146 int lastSlashIndex = defaultPath.lastIndexOf(PATH_DELIM);
147 if (lastSlashIndex >= 0) {
148 if (lastSlashIndex == 0) {
149
150 lastSlashIndex = 1;
151 }
152 defaultPath = defaultPath.substring(0, lastSlashIndex);
153 }
154
155 HeaderElement[] headerElements = null;
156
157 boolean isNetscapeCookie = false;
158 int i1 = header.toLowerCase().indexOf("expires=");
159 if (i1 != -1) {
160 i1 += "expires=".length();
161 int i2 = header.indexOf(";", i1);
162 if (i2 == -1) {
163 i2 = header.length();
164 }
165 try {
166 DateUtil.parseDate(header.substring(i1, i2), this.datepatterns);
167 isNetscapeCookie = true;
168 } catch (DateParseException e) {
169
170 }
171 }
172 if (isNetscapeCookie) {
173 headerElements = new HeaderElement[] {
174 new HeaderElement(header.toCharArray())
175 };
176 } else {
177 headerElements = HeaderElement.parseElements(header.toCharArray());
178 }
179
180 Cookie[] cookies = new Cookie[headerElements.length];
181
182 for (int i = 0; i < headerElements.length; i++) {
183
184 HeaderElement headerelement = headerElements[i];
185 Cookie cookie = null;
186 try {
187 cookie = new Cookie(host,
188 headerelement.getName(),
189 headerelement.getValue(),
190 defaultPath,
191 null,
192 false);
193 } catch (IllegalArgumentException e) {
194 throw new MalformedCookieException(e.getMessage());
195 }
196
197 NameValuePair[] parameters = headerelement.getParameters();
198
199 if (parameters != null) {
200
201 for (int j = 0; j < parameters.length; j++) {
202 parseAttribute(parameters[j], cookie);
203 }
204 }
205 cookies[i] = cookie;
206 }
207 return cookies;
208 }
209
210
211 /***
212 * Parse the <tt>"Set-Cookie"</tt> {@link Header} into an array of {@link
213 * Cookie}s.
214 *
215 * <P>The syntax for the Set-Cookie response header is:
216 *
217 * <PRE>
218 * set-cookie = "Set-Cookie:" cookies
219 * cookies = 1#cookie
220 * cookie = NAME "=" VALUE * (";" cookie-av)
221 * NAME = attr
222 * VALUE = value
223 * cookie-av = "Comment" "=" value
224 * | "Domain" "=" value
225 * | "Max-Age" "=" value
226 * | "Path" "=" value
227 * | "Secure"
228 * | "Version" "=" 1*DIGIT
229 * </PRE>
230 *
231 * @param host the host from which the <tt>Set-Cookie</tt> header was
232 * received
233 * @param port the port from which the <tt>Set-Cookie</tt> header was
234 * received
235 * @param path the path from which the <tt>Set-Cookie</tt> header was
236 * received
237 * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> header was
238 * received over secure conection
239 * @param header the <tt>Set-Cookie</tt> received from the server
240 * @return an array of <tt>Cookie</tt>s parsed from the <tt>"Set-Cookie"
241 * </tt> header
242 * @throws MalformedCookieException if an exception occurs during parsing
243 */
244 public Cookie[] parse(
245 String host, int port, String path, boolean secure, final Header header)
246 throws MalformedCookieException {
247
248 LOG.trace("enter CookieSpecBase.parse("
249 + "String, port, path, boolean, String)");
250 if (header == null) {
251 throw new IllegalArgumentException("Header may not be null.");
252 }
253 return parse(host, port, path, secure, header.getValue());
254 }
255
256
257 /***
258 * Parse the cookie attribute and update the corresponsing {@link Cookie}
259 * properties.
260 *
261 * @param attribute {@link HeaderElement} cookie attribute from the
262 * <tt>Set- Cookie</tt>
263 * @param cookie {@link Cookie} to be updated
264 * @throws MalformedCookieException if an exception occurs during parsing
265 */
266
267 public void parseAttribute(
268 final NameValuePair attribute, final Cookie cookie)
269 throws MalformedCookieException {
270
271 if (attribute == null) {
272 throw new IllegalArgumentException("Attribute may not be null.");
273 }
274 if (cookie == null) {
275 throw new IllegalArgumentException("Cookie may not be null.");
276 }
277 final String paramName = attribute.getName().toLowerCase();
278 String paramValue = attribute.getValue();
279
280 if (paramName.equals("path")) {
281
282 if ((paramValue == null) || (paramValue.trim().equals(""))) {
283 paramValue = "/";
284 }
285 cookie.setPath(paramValue);
286 cookie.setPathAttributeSpecified(true);
287
288 } else if (paramName.equals("domain")) {
289
290 if (paramValue == null) {
291 throw new MalformedCookieException(
292 "Missing value for domain attribute");
293 }
294 if (paramValue.trim().equals("")) {
295 throw new MalformedCookieException(
296 "Blank value for domain attribute");
297 }
298 cookie.setDomain(paramValue);
299 cookie.setDomainAttributeSpecified(true);
300
301 } else if (paramName.equals("max-age")) {
302
303 if (paramValue == null) {
304 throw new MalformedCookieException(
305 "Missing value for max-age attribute");
306 }
307 int age;
308 try {
309 age = Integer.parseInt(paramValue);
310 } catch (NumberFormatException e) {
311 throw new MalformedCookieException ("Invalid max-age "
312 + "attribute: " + e.getMessage());
313 }
314 cookie.setExpiryDate(
315 new Date(System.currentTimeMillis() + age * 1000L));
316
317 } else if (paramName.equals("secure")) {
318
319 cookie.setSecure(true);
320
321 } else if (paramName.equals("comment")) {
322
323 cookie.setComment(paramValue);
324
325 } else if (paramName.equals("expires")) {
326
327 if (paramValue == null) {
328 throw new MalformedCookieException(
329 "Missing value for expires attribute");
330 }
331
332 try {
333 cookie.setExpiryDate(DateUtil.parseDate(paramValue, this.datepatterns));
334 } catch (DateParseException dpe) {
335 LOG.debug("Error parsing cookie date", dpe);
336 throw new MalformedCookieException(
337 "Unable to parse expiration date parameter: "
338 + paramValue);
339 }
340 } else {
341 if (LOG.isDebugEnabled()) {
342 LOG.debug("Unrecognized cookie attribute: "
343 + attribute.toString());
344 }
345 }
346 }
347
348
349 public Collection getValidDateFormats() {
350 return this.datepatterns;
351 }
352
353 public void setValidDateFormats(final Collection datepatterns) {
354 this.datepatterns = datepatterns;
355 }
356
357 /***
358 * Performs most common {@link Cookie} validation
359 *
360 * @param host the host from which the {@link Cookie} was received
361 * @param port the port from which the {@link Cookie} was received
362 * @param path the path from which the {@link Cookie} was received
363 * @param secure <tt>true</tt> when the {@link Cookie} was received using a
364 * secure connection
365 * @param cookie The cookie to validate.
366 * @throws MalformedCookieException if an exception occurs during
367 * validation
368 */
369
370 public void validate(String host, int port, String path,
371 boolean secure, final Cookie cookie)
372 throws MalformedCookieException {
373
374 LOG.trace("enter CookieSpecBase.validate("
375 + "String, port, path, boolean, Cookie)");
376 if (host == null) {
377 throw new IllegalArgumentException(
378 "Host of origin may not be null");
379 }
380 if (host.trim().equals("")) {
381 throw new IllegalArgumentException(
382 "Host of origin may not be blank");
383 }
384 if (port < 0) {
385 throw new IllegalArgumentException("Invalid port: " + port);
386 }
387 if (path == null) {
388 throw new IllegalArgumentException(
389 "Path of origin may not be null.");
390 }
391 if (path.trim().equals("")) {
392 path = PATH_DELIM;
393 }
394 host = host.toLowerCase();
395
396 if (cookie.getVersion() < 0) {
397 throw new MalformedCookieException ("Illegal version number "
398 + cookie.getValue());
399 }
400
401
402
403
404
405
406
407
408
409 if (host.indexOf(".") >= 0) {
410
411
412
413
414 if (!host.endsWith(cookie.getDomain())) {
415 String s = cookie.getDomain();
416 if (s.startsWith(".")) {
417 s = s.substring(1, s.length());
418 }
419 if (!host.equals(s)) {
420 throw new MalformedCookieException(
421 "Illegal domain attribute \"" + cookie.getDomain()
422 + "\". Domain of origin: \"" + host + "\"");
423 }
424 }
425
426 else {
427
428
429 String requestedDomain = cookie.getDomain();
430 if(requestedDomain.startsWith(".")) {
431 requestedDomain = requestedDomain.substring(1);
432 }
433 try {
434 if((InternetDomainName.fromLenient(requestedDomain)).isPublicSuffix()) {
435 throw new MalformedCookieException(
436 "Illegal public-suffix domain attribute \"" + cookie.getDomain()
437 + "\". Domain of origin: \"" + host + "\"");
438 }
439 } catch (IllegalArgumentException e) {
440
441 }
442 }
443
444 } else {
445 if (!host.equals(cookie.getDomain())) {
446 throw new MalformedCookieException(
447 "Illegal domain attribute \"" + cookie.getDomain()
448 + "\". Domain of origin: \"" + host + "\"");
449 }
450 }
451
452
453
454
455 if (!path.startsWith(cookie.getPath())) {
456 throw new MalformedCookieException(
457 "Illegal path attribute \"" + cookie.getPath()
458 + "\". Path of origin: \"" + path + "\"");
459 }
460 }
461
462
463 /***
464 * Return <tt>true</tt> if the cookie should be submitted with a request
465 * with given attributes, <tt>false</tt> otherwise.
466 * @param host the host to which the request is being submitted
467 * @param port the port to which the request is being submitted (ignored)
468 * @param path the path to which the request is being submitted
469 * @param secure <tt>true</tt> if the request is using a secure connection
470 * @param cookie {@link Cookie} to be matched
471 * @return true if the cookie matches the criterium
472 */
473
474 public boolean match(String host, int port, String path,
475 boolean secure, final Cookie cookie) {
476
477 LOG.trace("enter CookieSpecBase.match("
478 + "String, int, String, boolean, Cookie");
479
480 if (host == null) {
481 throw new IllegalArgumentException(
482 "Host of origin may not be null");
483 }
484 if (host.trim().equals("")) {
485 throw new IllegalArgumentException(
486 "Host of origin may not be blank");
487 }
488 if (port < 0) {
489 throw new IllegalArgumentException("Invalid port: " + port);
490 }
491 if (path == null) {
492 throw new IllegalArgumentException(
493 "Path of origin may not be null.");
494 }
495 if (cookie == null) {
496 throw new IllegalArgumentException("Cookie may not be null");
497 }
498 if (path.trim().equals("")) {
499 path = PATH_DELIM;
500 }
501 host = host.toLowerCase();
502 if (cookie.getDomain() == null) {
503 LOG.warn("Invalid cookie state: domain not specified");
504 return false;
505 }
506 if (cookie.getPath() == null) {
507 LOG.warn("Invalid cookie state: path not specified");
508 return false;
509 }
510
511 return
512
513 (cookie.getExpiryDate() == null
514 || cookie.getExpiryDate().after(new Date()))
515
516 && (domainMatch(host, cookie.getDomain()))
517
518 && (pathMatch(path, cookie.getPath()))
519
520
521 && (cookie.getSecure() ? secure : true);
522 }
523
524 /***
525 * Performs domain-match as implemented in common browsers.
526 * @param host The target host.
527 * @param domain The cookie domain attribute.
528 * @return true if the specified host matches the given domain.
529 */
530 public boolean domainMatch(final String host, String domain) {
531 if (host.equals(domain)) {
532 return true;
533 }
534 if (!domain.startsWith(".")) {
535 domain = "." + domain;
536 }
537 return host.endsWith(domain) || host.equals(domain.substring(1));
538 }
539
540 /***
541 * Performs path-match as implemented in common browsers.
542 * @param path The target path.
543 * @param topmostPath The cookie path attribute.
544 * @return true if the paths match
545 */
546 public boolean pathMatch(final String path, final String topmostPath) {
547 boolean match = path.startsWith (topmostPath);
548
549
550 if (match && path.length() != topmostPath.length()) {
551 if (!topmostPath.endsWith(PATH_DELIM)) {
552 match = (path.charAt(topmostPath.length()) == PATH_DELIM_CHAR);
553 }
554 }
555 return match;
556 }
557
558 /***
559 * Return an array of {@link Cookie}s that should be submitted with a
560 * request with given attributes, <tt>false</tt> otherwise.
561 * @param host the host to which the request is being submitted
562 * @param port the port to which the request is being submitted (currently
563 * ignored)
564 * @param path the path to which the request is being submitted
565 * @param secure <tt>true</tt> if the request is using a secure protocol
566 * @param cookies an array of <tt>Cookie</tt>s to be matched
567 * @return an array of <tt>Cookie</tt>s matching the criterium
568 *
569 // BEGIN IA/HERITRIX CHANGES
570 * @deprecated use match(String, int, String, boolean, SortedMap)
571 // END IA/HERITRIX CHANGES
572 */
573
574 public Cookie[] match(String host, int port, String path,
575 boolean secure, final Cookie cookies[]) {
576
577 LOG.trace("enter CookieSpecBase.match("
578 + "String, int, String, boolean, Cookie[])");
579
580 if (cookies == null) {
581 return null;
582 }
583 List matching = new LinkedList();
584 for (int i = 0; i < cookies.length; i++) {
585 if (match(host, port, path, secure, cookies[i])) {
586 addInPathOrder(matching, cookies[i]);
587 }
588 }
589 return (Cookie[]) matching.toArray(new Cookie[matching.size()]);
590 }
591
592
593 /***
594 * Return an array of {@link Cookie}s that should be submitted with a
595 * request with given attributes, <tt>false</tt> otherwise.
596 *
597 * If the SortedMap comes from an HttpState and is not itself
598 * thread-safe, it may be necessary to synchronize on the HttpState
599 * instance to protect against concurrent modification.
600 *
601 * @param host the host to which the request is being submitted
602 * @param port the port to which the request is being submitted (currently
603 * ignored)
604 * @param path the path to which the request is being submitted
605 * @param secure <tt>true</tt> if the request is using a secure protocol
606 * @param cookies SortedMap of <tt>Cookie</tt>s to be matched
607 * @return an array of <tt>Cookie</tt>s matching the criterium
608 */
609
610 public Cookie[] match(String host, int port, String path,
611 boolean secure, final SortedMap cookies) {
612
613 LOG.trace("enter CookieSpecBase.match("
614 + "String, int, String, boolean, SortedMap)");
615
616
617
618
619 if (cookies == null) {
620 return null;
621 }
622 List matching = new LinkedList();
623 InternetDomainName domain;
624 try {
625 domain = InternetDomainName.fromLenient(host);
626 } catch(IllegalArgumentException e) {
627 domain = null;
628 }
629
630 String candidate = (domain!=null) ? domain.toString() : host;
631 while(candidate!=null) {
632 Iterator iter = cookies.subMap(candidate,
633 candidate + Cookie.DOMAIN_OVERBOUNDS).values().iterator();
634 while (iter.hasNext()) {
635 Cookie cookie = (Cookie) (iter.next());
636 if (match(host, port, path, secure, cookie)) {
637 addInPathOrder(matching, cookie);
638 }
639 }
640 StoredIterator.close(iter);
641 if(domain!=null && domain.isUnderPublicSuffix()) {
642 domain = domain.parent();
643 candidate = domain.toString();
644 } else {
645 candidate = null;
646 }
647 }
648
649 return (Cookie[]) matching.toArray(new Cookie[matching.size()]);
650 }
651
652
653 /***
654 * Adds the given cookie into the given list in descending path order. That
655 * is, more specific path to least specific paths. This may not be the
656 * fastest algorythm, but it'll work OK for the small number of cookies
657 * we're generally dealing with.
658 *
659 * @param list - the list to add the cookie to
660 * @param addCookie - the Cookie to add to list
661 */
662 private static void addInPathOrder(List list, Cookie addCookie) {
663 int i = 0;
664
665 for (i = 0; i < list.size(); i++) {
666 Cookie c = (Cookie) list.get(i);
667 if (addCookie.compare(addCookie, c) > 0) {
668 break;
669 }
670 }
671 list.add(i, addCookie);
672 }
673
674 /***
675 * Return a string suitable for sending in a <tt>"Cookie"</tt> header
676 * @param cookie a {@link Cookie} to be formatted as string
677 * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
678 */
679 public String formatCookie(Cookie cookie) {
680 LOG.trace("enter CookieSpecBase.formatCookie(Cookie)");
681 if (cookie == null) {
682 throw new IllegalArgumentException("Cookie may not be null");
683 }
684 StringBuffer buf = new StringBuffer();
685 buf.append(cookie.getName());
686 buf.append("=");
687 String s = cookie.getValue();
688 if (s != null) {
689 buf.append(s);
690 }
691 return buf.toString();
692 }
693
694 /***
695 * Create a <tt>"Cookie"</tt> header value containing all {@link Cookie}s in
696 * <i>cookies</i> suitable for sending in a <tt>"Cookie"</tt> header
697 * @param cookies an array of {@link Cookie}s to be formatted
698 * @return a string suitable for sending in a Cookie header.
699 * @throws IllegalArgumentException if an input parameter is illegal
700 */
701
702 public String formatCookies(Cookie[] cookies)
703 throws IllegalArgumentException {
704 LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])");
705 if (cookies == null) {
706 throw new IllegalArgumentException("Cookie array may not be null");
707 }
708 if (cookies.length == 0) {
709 throw new IllegalArgumentException("Cookie array may not be empty");
710 }
711
712 StringBuffer buffer = new StringBuffer();
713 for (int i = 0; i < cookies.length; i++) {
714 if (i > 0) {
715 buffer.append("; ");
716 }
717 buffer.append(formatCookie(cookies[i]));
718 }
719 return buffer.toString();
720 }
721
722
723 /***
724 * Create a <tt>"Cookie"</tt> {@link Header} containing all {@link Cookie}s
725 * in <i>cookies</i>.
726 * @param cookies an array of {@link Cookie}s to be formatted as a <tt>"
727 * Cookie"</tt> header
728 * @return a <tt>"Cookie"</tt> {@link Header}.
729 */
730 public Header formatCookieHeader(Cookie[] cookies) {
731 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])");
732 return new Header("Cookie", formatCookies(cookies));
733 }
734
735
736 /***
737 * Create a <tt>"Cookie"</tt> {@link Header} containing the {@link Cookie}.
738 * @param cookie <tt>Cookie</tt>s to be formatted as a <tt>Cookie</tt>
739 * header
740 * @return a Cookie header.
741 */
742 public Header formatCookieHeader(Cookie cookie) {
743 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)");
744 return new Header("Cookie", formatCookie(cookie));
745 }
746
747 }