1 /*
2 * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java,v 1.14 2003/04/17 03:00:31 mbecke Exp $
3 * $Revision: 1.14 $
4 * $Date: 2003/04/17 03:00:31 $
5 *
6 * ====================================================================
7 *
8 * The Apache Software License, Version 1.1
9 *
10 * Copyright (c) 2002-2003 The Apache Software Foundation. All rights
11 * reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions
15 * are met:
16 *
17 * 1. Redistributions of source code must retain the above copyright
18 * notice, this list of conditions and the following disclaimer.
19 *
20 * 2. Redistributions in binary form must reproduce the above copyright
21 * notice, this list of conditions and the following disclaimer in
22 * the documentation and/or other materials provided with the
23 * distribution.
24 *
25 * 3. The end-user documentation included with the redistribution, if
26 * any, must include the following acknowlegement:
27 * "This product includes software developed by the
28 * Apache Software Foundation (http://www.apache.org/)."
29 * Alternately, this acknowlegement may appear in the software itself,
30 * if and wherever such third-party acknowlegements normally appear.
31 *
32 * 4. The names "The Jakarta Project", "Commons", and "Apache Software
33 * Foundation" must not be used to endorse or promote products derived
34 * from this software without prior written permission. For written
35 * permission, please contact apache@apache.org.
36 *
37 * 5. Products derived from this software may not be called "Apache"
38 * nor may "Apache" appear in their names without prior written
39 * permission of the Apache Group.
40 *
41 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
42 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
43 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
44 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
45 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
46 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
47 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
48 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
49 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
50 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
51 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
52 * SUCH DAMAGE.
53 * ====================================================================
54 *
55 * This software consists of voluntary contributions made by many
56 * individuals on behalf of the Apache Software Foundation. For more
57 * information on the Apache Software Foundation, please see
58 * <http://www.apache.org/>.
59 *
60 * [Additional notices, if required by prior licensing conditions]
61 *
62 */
63
64 package org.apache.commons.httpclient.cookie;
65
66 import java.util.Date;
67 import java.util.LinkedList;
68 import java.util.List;
69
70 import org.apache.commons.httpclient.Cookie;
71 import org.apache.commons.httpclient.Header;
72 import org.apache.commons.httpclient.HeaderElement;
73 import org.apache.commons.httpclient.HttpException;
74 import org.apache.commons.httpclient.NameValuePair;
75 import org.apache.commons.httpclient.util.DateParseException;
76 import org.apache.commons.httpclient.util.DateParser;
77 import org.apache.commons.logging.Log;
78 import org.apache.commons.logging.LogFactory;
79
80 /***
81 *
82 * Cookie management functions shared by all specification.
83 *
84 * @author B.C. Holmes
85 * @author <a href="mailto:jericho@thinkfree.com">Park, Sung-Gu</a>
86 * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a>
87 * @author Rod Waldhoff
88 * @author dIon Gillard
89 * @author Sean C. Sullivan
90 * @author <a href="mailto:JEvans@Cyveillance.com">John Evans</a>
91 * @author Marc A. Saegesser
92 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
93 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
94 *
95 * @since 2.0
96 */
97 public class CookieSpecBase implements CookieSpec {
98
99 /*** Log object */
100 protected static final Log LOG = LogFactory.getLog(CookieSpec.class);
101
102 /*** Default constructor */
103 public CookieSpecBase() {
104 super();
105 }
106
107
108 /***
109 * Parses the Set-Cookie value into an array of <tt>Cookie</tt>s.
110 *
111 * <P>The syntax for the Set-Cookie response header is:
112 *
113 * <PRE>
114 * set-cookie = "Set-Cookie:" cookies
115 * cookies = 1#cookie
116 * cookie = NAME "=" VALUE * (";" cookie-av)
117 * NAME = attr
118 * VALUE = value
119 * cookie-av = "Comment" "=" value
120 * | "Domain" "=" value
121 * | "Max-Age" "=" value
122 * | "Path" "=" value
123 * | "Secure"
124 * | "Version" "=" 1*DIGIT
125 * </PRE>
126 *
127 * @param host the host from which the <tt>Set-Cookie</tt> value was
128 * received
129 * @param port the port from which the <tt>Set-Cookie</tt> value was
130 * received
131 * @param path the path from which the <tt>Set-Cookie</tt> value was
132 * received
133 * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> value was
134 * received over secure conection
135 * @param header the <tt>Set-Cookie</tt> received from the server
136 * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie value
137 * @throws MalformedCookieException if an exception occurs during parsing
138 */
139 public Cookie[] parse(String host, int port, String path,
140 boolean secure, final String header)
141 throws MalformedCookieException {
142
143 LOG.trace("enter CookieSpecBase.parse("
144 + "String, port, path, boolean, Header)");
145
146 if (host == null) {
147 throw new IllegalArgumentException(
148 "Host of origin may not be null");
149 }
150 if (host.trim().equals("")) {
151 throw new IllegalArgumentException(
152 "Host of origin may not be blank");
153 }
154 if (port < 0) {
155 throw new IllegalArgumentException("Invalid port: " + port);
156 }
157 if (path == null) {
158 throw new IllegalArgumentException(
159 "Path of origin may not be null.");
160 }
161 if (header == null) {
162 throw new IllegalArgumentException("Header may not be null.");
163 }
164
165 if (path.trim().equals("")) {
166 path = PATH_DELIM;
167 }
168 host = host.toLowerCase();
169
170 HeaderElement[] headerElements = null;
171 try {
172 headerElements = HeaderElement.parse(header);
173 } catch (HttpException e) {
174 throw new MalformedCookieException(e.getMessage());
175 }
176
177 String defaultPath = path;
178 int lastSlashIndex = defaultPath.lastIndexOf(PATH_DELIM);
179 if (lastSlashIndex > 0) {
180 defaultPath = defaultPath.substring(0, lastSlashIndex);
181 }
182
183 Cookie[] cookies = new Cookie[headerElements.length];
184
185 for (int i = 0; i < headerElements.length; i++) {
186
187 HeaderElement headerelement = headerElements[i];
188 Cookie cookie = new Cookie(host,
189 headerelement.getName(),
190 headerelement.getValue(),
191 defaultPath,
192 null,
193 false);
194
195 // cycle through the parameters
196 NameValuePair[] parameters = headerelement.getParameters();
197 // could be null. In case only a header element and no parameters.
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) {
282 throw new MalformedCookieException(
283 "Missing value for path attribute");
284 }
285 if (paramValue.trim().equals("")) {
286 throw new MalformedCookieException(
287 "Blank value for path attribute");
288 }
289 cookie.setPath(paramValue);
290 cookie.setPathAttributeSpecified(true);
291
292 } else if (paramName.equals("domain")) {
293
294 if (paramValue == null) {
295 throw new MalformedCookieException(
296 "Missing value for domain attribute");
297 }
298 if (paramValue.trim().equals("")) {
299 throw new MalformedCookieException(
300 "Blank value for domain attribute");
301 }
302 cookie.setDomain(paramValue);
303 cookie.setDomainAttributeSpecified(true);
304
305 } else if (paramName.equals("max-age")) {
306
307 if (paramValue == null) {
308 throw new MalformedCookieException(
309 "Missing value for max-age attribute");
310 }
311 int age;
312 try {
313 age = Integer.parseInt(paramValue);
314 } catch (NumberFormatException e) {
315 throw new MalformedCookieException ("Invalid max-age "
316 + "attribute: " + e.getMessage());
317 }
318 cookie.setExpiryDate(
319 new Date(System.currentTimeMillis() + age * 1000L));
320
321 } else if (paramName.equals("secure")) {
322
323 cookie.setSecure(true);
324
325 } else if (paramName.equals("comment")) {
326
327 cookie.setComment(paramValue);
328
329 } else if (paramName.equals("expires")) {
330
331 if (paramValue == null) {
332 throw new MalformedCookieException(
333 "Missing value for expires attribute");
334 }
335 // trim single quotes around expiry if present
336 // see http://nagoya.apache.org/bugzilla/show_bug.cgi?id=5279
337 if (paramValue.length() > 1
338 && paramValue.startsWith("'")
339 && paramValue.endsWith("'")) {
340 paramValue
341 = paramValue.substring (1, paramValue.length() - 1);
342 }
343
344 try {
345 cookie.setExpiryDate(DateParser.parseDate(paramValue));
346 } catch (DateParseException dpe) {
347 LOG.debug("Error parsing cookie date", dpe);
348 throw new MalformedCookieException(
349 "Unable to parse expiration date parameter: "
350 + paramValue);
351 }
352 } else {
353 if (LOG.isDebugEnabled()) {
354 LOG.debug("Unrecognized cookie attribute: "
355 + attribute.toString());
356 }
357 }
358 }
359
360
361 /***
362 * Performs most common {@link Cookie} validation
363 *
364 * @param host the host from which the {@link Cookie} was received
365 * @param port the port from which the {@link Cookie} was received
366 * @param path the path from which the {@link Cookie} was received
367 * @param secure <tt>true</tt> when the {@link Cookie} was received using a
368 * secure connection
369 * @param cookie The cookie to validate.
370 * @throws MalformedCookieException if an exception occurs during
371 * validation
372 */
373
374 public void validate(String host, int port, String path,
375 boolean secure, final Cookie cookie)
376 throws MalformedCookieException {
377
378 LOG.trace("enter CookieSpecBase.validate("
379 + "String, port, path, boolean, Cookie)");
380 if (host == null) {
381 throw new IllegalArgumentException(
382 "Host of origin may not be null");
383 }
384 if (host.trim().equals("")) {
385 throw new IllegalArgumentException(
386 "Host of origin may not be blank");
387 }
388 if (port < 0) {
389 throw new IllegalArgumentException("Invalid port: " + port);
390 }
391 if (path == null) {
392 throw new IllegalArgumentException(
393 "Path of origin may not be null.");
394 }
395 if (path.trim().equals("")) {
396 path = PATH_DELIM;
397 }
398 host = host.toLowerCase();
399 // check version
400 if (cookie.getVersion() < 0) {
401 throw new MalformedCookieException ("Illegal version number "
402 + cookie.getValue());
403 }
404
405 // security check... we musn't allow the server to give us an
406 // invalid domain scope
407
408 // Validate the cookies domain attribute. NOTE: Domains without
409 // any dots are allowed to support hosts on private LANs that don't
410 // have DNS names. Since they have no dots, to domain-match the
411 // request-host and domain must be identical for the cookie to sent
412 // back to the origin-server.
413 if (host.indexOf(".") >= 0) {
414 // Not required to have at least two dots. RFC 2965.
415 // A Set-Cookie2 with Domain=ajax.com will be accepted.
416
417 // domain must match host
418 if (!host.endsWith(cookie.getDomain())) {
419 throw new MalformedCookieException(
420 "Illegal domain attribute \"" + cookie.getDomain()
421 + "\". Domain of origin: \"" + host + "\"");
422 }
423 } else {
424 if (!host.equals(cookie.getDomain())) {
425 throw new MalformedCookieException(
426 "Illegal domain attribute \"" + cookie.getDomain()
427 + "\". Domain of origin: \"" + host + "\"");
428 }
429 }
430
431 // another security check... we musn't allow the server to give us a
432 // cookie that doesn't match this path
433
434 if (!path.startsWith(cookie.getPath())) {
435 throw new MalformedCookieException(
436 "Illegal path attribute \"" + cookie.getPath()
437 + "\". Path of origin: \"" + path + "\"");
438 }
439 }
440
441
442 /***
443 * Return <tt>true</tt> if the cookie should be submitted with a request
444 * with given attributes, <tt>false</tt> otherwise.
445 * @param host the host to which the request is being submitted
446 * @param port the port to which the request is being submitted (ignored)
447 * @param path the path to which the request is being submitted
448 * @param secure <tt>true</tt> if the request is using a secure connection
449 * @param cookie {@link Cookie} to be matched
450 * @return true if the cookie matches the criterium
451 */
452
453 public boolean match(String host, int port, String path,
454 boolean secure, final Cookie cookie) {
455
456 LOG.trace("enter CookieSpecBase.match("
457 + "String, int, String, boolean, Cookie");
458
459 if (host == null) {
460 throw new IllegalArgumentException(
461 "Host of origin may not be null");
462 }
463 if (host.trim().equals("")) {
464 throw new IllegalArgumentException(
465 "Host of origin may not be blank");
466 }
467 if (port < 0) {
468 throw new IllegalArgumentException("Invalid port: " + port);
469 }
470 if (path == null) {
471 throw new IllegalArgumentException(
472 "Path of origin may not be null.");
473 }
474 if (cookie == null) {
475 throw new IllegalArgumentException("Cookie may not be null");
476 }
477 if (path.trim().equals("")) {
478 path = PATH_DELIM;
479 }
480 host = host.toLowerCase();
481 if (cookie.getDomain() == null) {
482 LOG.warn("Invalid cookie state: domain not specified");
483 return false;
484 }
485 if (cookie.getPath() == null) {
486 LOG.warn("Invalid cookie state: path not specified");
487 return false;
488 }
489
490 return
491 // only add the cookie if it hasn't yet expired
492 (cookie.getExpiryDate() == null
493 || cookie.getExpiryDate().after(new Date()))
494 // and the domain pattern matches
495 && (domainMatch(host, cookie.getDomain()))
496 // and the path is null or matching
497 && (pathMatch(path, cookie.getPath()))
498 // and if the secure flag is set, only if the request is
499 // actually secure
500 && (cookie.getSecure() ? secure : true);
501 }
502
503 /***
504 * Performs a domain-match as described in RFC2109.
505 * @param host The host to check.
506 * @param domain The domain.
507 * @return true if the specified host matches the given domain.
508 */
509 private static boolean domainMatch(String host, String domain) {
510 boolean match = host.equals(domain)
511 || (domain.startsWith(".") && host.endsWith(domain));
512
513 return match;
514 }
515
516 /***
517 * Performs a path-match slightly smarter than a straight-forward startsWith
518 * check.
519 * @param path The path to check.
520 * @param topmostPath The path to check against.
521 * @return true if the paths match
522 */
523 private static boolean pathMatch(
524 final String path, final String topmostPath) {
525
526 boolean match = path.startsWith (topmostPath);
527
528 // if there is a match and these values are not exactly the same we have
529 // to make sure we're not matcing "/foobar" and "/foo"
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
550 public Cookie[] match(String host, int port, String path,
551 boolean secure, final Cookie cookies[]) {
552
553 LOG.trace("enter CookieSpecBase.match("
554 + "String, int, String, boolean, Cookie[])");
555
556 if (host == null) {
557 throw new IllegalArgumentException(
558 "Host of origin may not be null");
559 }
560 if (host.trim().equals("")) {
561 throw new IllegalArgumentException(
562 "Host of origin may not be blank");
563 }
564 if (port < 0) {
565 throw new IllegalArgumentException("Invalid port: " + port);
566 }
567 if (path == null) {
568 throw new IllegalArgumentException(
569 "Path of origin may not be null.");
570 }
571 if (cookies == null) {
572 throw new IllegalArgumentException("Cookie array may not be null");
573 }
574 if (path.trim().equals("")) {
575 path = PATH_DELIM;
576 }
577 host = host.toLowerCase();
578
579 if (cookies.length <= 0) {
580 return null;
581 }
582 List matching = new LinkedList();
583 for (int i = 0; i < cookies.length; i++) {
584 if (match(host, port, path, secure, cookies[i])) {
585 addInPathOrder(matching, cookies[i]);
586 }
587 }
588 return (Cookie[]) matching.toArray(new Cookie[matching.size()]);
589 }
590
591
592 /***
593 * Adds the given cookie into the given list in descending path order. That
594 * is, more specific path to least specific paths. This may not be the
595 * fastest algorythm, but it'll work OK for the small number of cookies
596 * we're generally dealing with.
597 *
598 * @param list - the list to add the cookie to
599 * @param addCookie - the Cookie to add to list
600 */
601 private static void addInPathOrder(List list, Cookie addCookie) {
602 int i = 0;
603
604 for (i = 0; i < list.size(); i++) {
605 Cookie c = (Cookie) list.get(i);
606 if (addCookie.compare(addCookie, c) > 0) {
607 break;
608 }
609 }
610 list.add(i, addCookie);
611 }
612
613 /***
614 * Return a string suitable for sending in a <tt>"Cookie"</tt> header
615 * @param cookie a {@link Cookie} to be formatted as string
616 * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
617 */
618 public String formatCookie(Cookie cookie) {
619 LOG.trace("enter CookieSpecBase.formatCookie(Cookie)");
620 if (cookie == null) {
621 throw new IllegalArgumentException("Cookie may not be null");
622 }
623 StringBuffer buf = new StringBuffer();
624 buf.append(cookie.getName());
625 buf.append("=");
626 String s = cookie.getValue();
627 if (s != null) {
628 buf.append(s);
629 };
630 return buf.toString();
631 }
632
633 /***
634 * Create a <tt>"Cookie"</tt> header value containing all {@link Cookie}s in
635 * <i>cookies</i> suitable for sending in a <tt>"Cookie"</tt> header
636 * @param cookies an array of {@link Cookie}s to be formatted
637 * @return a string suitable for sending in a Cookie header.
638 * @throws IllegalArgumentException if an input parameter is illegal
639 */
640
641 public String formatCookies(Cookie[] cookies)
642 throws IllegalArgumentException {
643 LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])");
644 if (cookies == null) {
645 throw new IllegalArgumentException("Cookie array may not be null");
646 }
647 if (cookies.length == 0) {
648 throw new IllegalArgumentException("Cookie array may not be empty");
649 }
650
651 StringBuffer buffer = new StringBuffer();
652 for (int i = 0; i < cookies.length; i++) {
653 if (i > 0) {
654 buffer.append("; ");
655 }
656 buffer.append(formatCookie(cookies[i]));
657 }
658 return buffer.toString();
659 }
660
661
662 /***
663 * Create a <tt>"Cookie"</tt> {@link Header} containing all {@link Cookie}s
664 * in <i>cookies</i>.
665 * @param cookies an array of {@link Cookie}s to be formatted as a <tt>"
666 * Cookie"</tt> header
667 * @return a <tt>"Cookie"</tt> {@link Header}.
668 */
669 public Header formatCookieHeader(Cookie[] cookies) {
670 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])");
671 return new Header("Cookie", formatCookies(cookies));
672 }
673
674
675 /***
676 * Create a <tt>"Cookie"</tt> {@link Header} containing the {@link Cookie}.
677 * @param cookie <tt>Cookie</tt>s to be formatted as a <tt>Cookie</tt>
678 * header
679 * @return a Cookie header.
680 */
681 public Header formatCookieHeader(Cookie cookie) {
682 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)");
683 return new Header("Cookie", formatCookie(cookie));
684 }
685
686 }
This page was automatically generated by Maven