View Javadoc

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