View Javadoc

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