View Javadoc

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