1 /*
2 * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java,v 1.16 2003/06/12 19:12:16 olegk Exp $
3 * $Revision: 1.16 $
4 * $Date: 2003/06/12 19:12:16 $
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 if (lastSlashIndex == 0) {
181 //Do not remove the very first slash
182 lastSlashIndex = 1;
183 }
184 defaultPath = defaultPath.substring(0, lastSlashIndex);
185 }
186
187 Cookie[] cookies = new Cookie[headerElements.length];
188
189 for (int i = 0; i < headerElements.length; i++) {
190
191 HeaderElement headerelement = headerElements[i];
192 Cookie cookie = new Cookie(host,
193 headerelement.getName(),
194 headerelement.getValue(),
195 defaultPath,
196 null,
197 false);
198
199 // cycle through the parameters
200 NameValuePair[] parameters = headerelement.getParameters();
201 // could be null. In case only a header element and no parameters.
202 if (parameters != null) {
203
204 for (int j = 0; j < parameters.length; j++) {
205 parseAttribute(parameters[j], cookie);
206 }
207 }
208 cookies[i] = cookie;
209 }
210 return cookies;
211 }
212
213
214 /***
215 * Parse the <tt>"Set-Cookie"</tt> {@link Header} into an array of {@link
216 * Cookie}s.
217 *
218 * <P>The syntax for the Set-Cookie response header is:
219 *
220 * <PRE>
221 * set-cookie = "Set-Cookie:" cookies
222 * cookies = 1#cookie
223 * cookie = NAME "=" VALUE * (";" cookie-av)
224 * NAME = attr
225 * VALUE = value
226 * cookie-av = "Comment" "=" value
227 * | "Domain" "=" value
228 * | "Max-Age" "=" value
229 * | "Path" "=" value
230 * | "Secure"
231 * | "Version" "=" 1*DIGIT
232 * </PRE>
233 *
234 * @param host the host from which the <tt>Set-Cookie</tt> header was
235 * received
236 * @param port the port from which the <tt>Set-Cookie</tt> header was
237 * received
238 * @param path the path from which the <tt>Set-Cookie</tt> header was
239 * received
240 * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> header was
241 * received over secure conection
242 * @param header the <tt>Set-Cookie</tt> received from the server
243 * @return an array of <tt>Cookie</tt>s parsed from the <tt>"Set-Cookie"
244 * </tt> header
245 * @throws MalformedCookieException if an exception occurs during parsing
246 */
247 public Cookie[] parse(
248 String host, int port, String path, boolean secure, final Header header)
249 throws MalformedCookieException {
250
251 LOG.trace("enter CookieSpecBase.parse("
252 + "String, port, path, boolean, String)");
253 if (header == null) {
254 throw new IllegalArgumentException("Header may not be null.");
255 }
256 return parse(host, port, path, secure, header.getValue());
257 }
258
259
260 /***
261 * Parse the cookie attribute and update the corresponsing {@link Cookie}
262 * properties.
263 *
264 * @param attribute {@link HeaderElement} cookie attribute from the
265 * <tt>Set- Cookie</tt>
266 * @param cookie {@link Cookie} to be updated
267 * @throws MalformedCookieException if an exception occurs during parsing
268 */
269
270 public void parseAttribute(
271 final NameValuePair attribute, final Cookie cookie)
272 throws MalformedCookieException {
273
274 if (attribute == null) {
275 throw new IllegalArgumentException("Attribute may not be null.");
276 }
277 if (cookie == null) {
278 throw new IllegalArgumentException("Cookie may not be null.");
279 }
280 final String paramName = attribute.getName().toLowerCase();
281 String paramValue = attribute.getValue();
282
283 if (paramName.equals("path")) {
284
285 if ((paramValue == null) || (paramValue.trim().equals(""))) {
286 paramValue = "/";
287 }
288 cookie.setPath(paramValue);
289 cookie.setPathAttributeSpecified(true);
290
291 } else if (paramName.equals("domain")) {
292
293 if (paramValue == null) {
294 throw new MalformedCookieException(
295 "Missing value for domain attribute");
296 }
297 if (paramValue.trim().equals("")) {
298 throw new MalformedCookieException(
299 "Blank value for domain attribute");
300 }
301 cookie.setDomain(paramValue);
302 cookie.setDomainAttributeSpecified(true);
303
304 } else if (paramName.equals("max-age")) {
305
306 if (paramValue == null) {
307 throw new MalformedCookieException(
308 "Missing value for max-age attribute");
309 }
310 int age;
311 try {
312 age = Integer.parseInt(paramValue);
313 } catch (NumberFormatException e) {
314 throw new MalformedCookieException ("Invalid max-age "
315 + "attribute: " + e.getMessage());
316 }
317 cookie.setExpiryDate(
318 new Date(System.currentTimeMillis() + age * 1000L));
319
320 } else if (paramName.equals("secure")) {
321
322 cookie.setSecure(true);
323
324 } else if (paramName.equals("comment")) {
325
326 cookie.setComment(paramValue);
327
328 } else if (paramName.equals("expires")) {
329
330 if (paramValue == null) {
331 throw new MalformedCookieException(
332 "Missing value for expires attribute");
333 }
334 // trim single quotes around expiry if present
335 // see http://nagoya.apache.org/bugzilla/show_bug.cgi?id=5279
336 if (paramValue.length() > 1
337 && paramValue.startsWith("'")
338 && paramValue.endsWith("'")) {
339 paramValue
340 = paramValue.substring (1, paramValue.length() - 1);
341 }
342
343 try {
344 cookie.setExpiryDate(DateParser.parseDate(paramValue));
345 } catch (DateParseException dpe) {
346 LOG.debug("Error parsing cookie date", dpe);
347 throw new MalformedCookieException(
348 "Unable to parse expiration date parameter: "
349 + paramValue);
350 }
351 } else {
352 if (LOG.isDebugEnabled()) {
353 LOG.debug("Unrecognized cookie attribute: "
354 + attribute.toString());
355 }
356 }
357 }
358
359
360 /***
361 * Performs most common {@link Cookie} validation
362 *
363 * @param host the host from which the {@link Cookie} was received
364 * @param port the port from which the {@link Cookie} was received
365 * @param path the path from which the {@link Cookie} was received
366 * @param secure <tt>true</tt> when the {@link Cookie} was received using a
367 * secure connection
368 * @param cookie The cookie to validate.
369 * @throws MalformedCookieException if an exception occurs during
370 * validation
371 */
372
373 public void validate(String host, int port, String path,
374 boolean secure, final Cookie cookie)
375 throws MalformedCookieException {
376
377 LOG.trace("enter CookieSpecBase.validate("
378 + "String, port, path, boolean, Cookie)");
379 if (host == null) {
380 throw new IllegalArgumentException(
381 "Host of origin may not be null");
382 }
383 if (host.trim().equals("")) {
384 throw new IllegalArgumentException(
385 "Host of origin may not be blank");
386 }
387 if (port < 0) {
388 throw new IllegalArgumentException("Invalid port: " + port);
389 }
390 if (path == null) {
391 throw new IllegalArgumentException(
392 "Path of origin may not be null.");
393 }
394 if (path.trim().equals("")) {
395 path = PATH_DELIM;
396 }
397 host = host.toLowerCase();
398 // check version
399 if (cookie.getVersion() < 0) {
400 throw new MalformedCookieException ("Illegal version number "
401 + cookie.getValue());
402 }
403
404 // security check... we musn't allow the server to give us an
405 // invalid domain scope
406
407 // Validate the cookies domain attribute. NOTE: Domains without
408 // any dots are allowed to support hosts on private LANs that don't
409 // have DNS names. Since they have no dots, to domain-match the
410 // request-host and domain must be identical for the cookie to sent
411 // back to the origin-server.
412 if (host.indexOf(".") >= 0) {
413 // Not required to have at least two dots. RFC 2965.
414 // A Set-Cookie2 with Domain=ajax.com will be accepted.
415
416 // domain must match host
417 if (!host.endsWith(cookie.getDomain())) {
418 throw new MalformedCookieException(
419 "Illegal domain attribute \"" + cookie.getDomain()
420 + "\". Domain of origin: \"" + host + "\"");
421 }
422 } else {
423 if (!host.equals(cookie.getDomain())) {
424 throw new MalformedCookieException(
425 "Illegal domain attribute \"" + cookie.getDomain()
426 + "\". Domain of origin: \"" + host + "\"");
427 }
428 }
429
430 // another security check... we musn't allow the server to give us a
431 // cookie that doesn't match this path
432
433 if (!path.startsWith(cookie.getPath())) {
434 throw new MalformedCookieException(
435 "Illegal path attribute \"" + cookie.getPath()
436 + "\". Path of origin: \"" + path + "\"");
437 }
438 }
439
440
441 /***
442 * Return <tt>true</tt> if the cookie should be submitted with a request
443 * with given attributes, <tt>false</tt> otherwise.
444 * @param host the host to which the request is being submitted
445 * @param port the port to which the request is being submitted (ignored)
446 * @param path the path to which the request is being submitted
447 * @param secure <tt>true</tt> if the request is using a secure connection
448 * @param cookie {@link Cookie} to be matched
449 * @return true if the cookie matches the criterium
450 */
451
452 public boolean match(String host, int port, String path,
453 boolean secure, final Cookie cookie) {
454
455 LOG.trace("enter CookieSpecBase.match("
456 + "String, int, String, boolean, Cookie");
457
458 if (host == null) {
459 throw new IllegalArgumentException(
460 "Host of origin may not be null");
461 }
462 if (host.trim().equals("")) {
463 throw new IllegalArgumentException(
464 "Host of origin may not be blank");
465 }
466 if (port < 0) {
467 throw new IllegalArgumentException("Invalid port: " + port);
468 }
469 if (path == null) {
470 throw new IllegalArgumentException(
471 "Path of origin may not be null.");
472 }
473 if (cookie == null) {
474 throw new IllegalArgumentException("Cookie may not be null");
475 }
476 if (path.trim().equals("")) {
477 path = PATH_DELIM;
478 }
479 host = host.toLowerCase();
480 if (cookie.getDomain() == null) {
481 LOG.warn("Invalid cookie state: domain not specified");
482 return false;
483 }
484 if (cookie.getPath() == null) {
485 LOG.warn("Invalid cookie state: path not specified");
486 return false;
487 }
488
489 return
490 // only add the cookie if it hasn't yet expired
491 (cookie.getExpiryDate() == null
492 || cookie.getExpiryDate().after(new Date()))
493 // and the domain pattern matches
494 && (domainMatch(host, cookie.getDomain()))
495 // and the path is null or matching
496 && (pathMatch(path, cookie.getPath()))
497 // and if the secure flag is set, only if the request is
498 // actually secure
499 && (cookie.getSecure() ? secure : true);
500 }
501
502 /***
503 * Performs a domain-match as described in RFC2109.
504 * @param host The host to check.
505 * @param domain The domain.
506 * @return true if the specified host matches the given domain.
507 */
508 private static boolean domainMatch(String host, String domain) {
509 boolean match = host.equals(domain)
510 || (domain.startsWith(".") && host.endsWith(domain));
511
512 return match;
513 }
514
515 /***
516 * Performs a path-match slightly smarter than a straight-forward startsWith
517 * check.
518 * @param path The path to check.
519 * @param topmostPath The path to check against.
520 * @return true if the paths match
521 */
522 private static boolean pathMatch(
523 final String path, final String topmostPath) {
524
525 boolean match = path.startsWith (topmostPath);
526
527 // if there is a match and these values are not exactly the same we have
528 // to make sure we're not matcing "/foobar" and "/foo"
529 if (match && path.length() != topmostPath.length()) {
530 if (!topmostPath.endsWith(PATH_DELIM)) {
531 match = (path.charAt(topmostPath.length()) == PATH_DELIM_CHAR);
532 }
533 }
534 return match;
535 }
536
537 /***
538 * Return an array of {@link Cookie}s that should be submitted with a
539 * request with given attributes, <tt>false</tt> otherwise.
540 * @param host the host to which the request is being submitted
541 * @param port the port to which the request is being submitted (currently
542 * ignored)
543 * @param path the path to which the request is being submitted
544 * @param secure <tt>true</tt> if the request is using a secure protocol
545 * @param cookies an array of <tt>Cookie</tt>s to be matched
546 * @return an array of <tt>Cookie</tt>s matching the criterium
547 */
548
549 public Cookie[] match(String host, int port, String path,
550 boolean secure, final Cookie cookies[]) {
551
552 LOG.trace("enter CookieSpecBase.match("
553 + "String, int, String, boolean, Cookie[])");
554
555 if (host == null) {
556 throw new IllegalArgumentException(
557 "Host of origin may not be null");
558 }
559 if (host.trim().equals("")) {
560 throw new IllegalArgumentException(
561 "Host of origin may not be blank");
562 }
563 if (port < 0) {
564 throw new IllegalArgumentException("Invalid port: " + port);
565 }
566 if (path == null) {
567 throw new IllegalArgumentException(
568 "Path of origin may not be null.");
569 }
570 if (cookies == null) {
571 throw new IllegalArgumentException("Cookie array may not be null");
572 }
573 if (path.trim().equals("")) {
574 path = PATH_DELIM;
575 }
576 host = host.toLowerCase();
577
578 if (cookies.length <= 0) {
579 return null;
580 }
581 List matching = new LinkedList();
582 for (int i = 0; i < cookies.length; i++) {
583 if (match(host, port, path, secure, cookies[i])) {
584 addInPathOrder(matching, cookies[i]);
585 }
586 }
587 return (Cookie[]) matching.toArray(new Cookie[matching.size()]);
588 }
589
590
591 /***
592 * Adds the given cookie into the given list in descending path order. That
593 * is, more specific path to least specific paths. This may not be the
594 * fastest algorythm, but it'll work OK for the small number of cookies
595 * we're generally dealing with.
596 *
597 * @param list - the list to add the cookie to
598 * @param addCookie - the Cookie to add to list
599 */
600 private static void addInPathOrder(List list, Cookie addCookie) {
601 int i = 0;
602
603 for (i = 0; i < list.size(); i++) {
604 Cookie c = (Cookie) list.get(i);
605 if (addCookie.compare(addCookie, c) > 0) {
606 break;
607 }
608 }
609 list.add(i, addCookie);
610 }
611
612 /***
613 * Return a string suitable for sending in a <tt>"Cookie"</tt> header
614 * @param cookie a {@link Cookie} to be formatted as string
615 * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
616 */
617 public String formatCookie(Cookie cookie) {
618 LOG.trace("enter CookieSpecBase.formatCookie(Cookie)");
619 if (cookie == null) {
620 throw new IllegalArgumentException("Cookie may not be null");
621 }
622 StringBuffer buf = new StringBuffer();
623 buf.append(cookie.getName());
624 buf.append("=");
625 String s = cookie.getValue();
626 if (s != null) {
627 buf.append(s);
628 };
629 return buf.toString();
630 }
631
632 /***
633 * Create a <tt>"Cookie"</tt> header value containing all {@link Cookie}s in
634 * <i>cookies</i> suitable for sending in a <tt>"Cookie"</tt> header
635 * @param cookies an array of {@link Cookie}s to be formatted
636 * @return a string suitable for sending in a Cookie header.
637 * @throws IllegalArgumentException if an input parameter is illegal
638 */
639
640 public String formatCookies(Cookie[] cookies)
641 throws IllegalArgumentException {
642 LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])");
643 if (cookies == null) {
644 throw new IllegalArgumentException("Cookie array may not be null");
645 }
646 if (cookies.length == 0) {
647 throw new IllegalArgumentException("Cookie array may not be empty");
648 }
649
650 StringBuffer buffer = new StringBuffer();
651 for (int i = 0; i < cookies.length; i++) {
652 if (i > 0) {
653 buffer.append("; ");
654 }
655 buffer.append(formatCookie(cookies[i]));
656 }
657 return buffer.toString();
658 }
659
660
661 /***
662 * Create a <tt>"Cookie"</tt> {@link Header} containing all {@link Cookie}s
663 * in <i>cookies</i>.
664 * @param cookies an array of {@link Cookie}s to be formatted as a <tt>"
665 * Cookie"</tt> header
666 * @return a <tt>"Cookie"</tt> {@link Header}.
667 */
668 public Header formatCookieHeader(Cookie[] cookies) {
669 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])");
670 return new Header("Cookie", formatCookies(cookies));
671 }
672
673
674 /***
675 * Create a <tt>"Cookie"</tt> {@link Header} containing the {@link Cookie}.
676 * @param cookie <tt>Cookie</tt>s to be formatted as a <tt>Cookie</tt>
677 * header
678 * @return a Cookie header.
679 */
680 public Header formatCookieHeader(Cookie cookie) {
681 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)");
682 return new Header("Cookie", formatCookie(cookie));
683 }
684
685 }
This page was automatically generated by Maven