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