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