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