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