View Javadoc

1   /*
2    * $HeadURL: https://svn.apache.org/repos/asf/httpcomponents/oac.hc3x/trunk/src/java/org/apache/commons/httpclient/cookie/RFC2965Spec.java $
3    * $Revision: 1425331 $
4    * $Date: 2012-12-22 19:29:41 +0100 (Sat, 22 Dec 2012) $
5    * 
6    * ====================================================================
7    *
8    *  Licensed to the Apache Software Foundation (ASF) under one or more
9    *  contributor license agreements.  See the NOTICE file distributed with
10   *  this work for additional information regarding copyright ownership.
11   *  The ASF licenses this file to You under the Apache License, Version 2.0
12   *  (the "License"); you may not use this file except in compliance with
13   *  the License.  You may obtain a copy of the License at
14   *
15   *      http://www.apache.org/licenses/LICENSE-2.0
16   *
17   *  Unless required by applicable law or agreed to in writing, software
18   *  distributed under the License is distributed on an "AS IS" BASIS,
19   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20   *  See the License for the specific language governing permissions and
21   *  limitations under the License.
22   * ====================================================================
23   *
24   * This software consists of voluntary contributions made by many
25   * individuals on behalf of the Apache Software Foundation.  For more
26   * information on the Apache Software Foundation, please see
27   * <http://www.apache.org/>.
28   *
29   */
30  
31  package org.apache.commons.httpclient.cookie;
32  
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.Comparator;
36  import java.util.Date;
37  import java.util.HashMap;
38  import java.util.Iterator;
39  import java.util.LinkedList;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.StringTokenizer;
43  
44  import org.apache.commons.httpclient.Cookie;
45  import org.apache.commons.httpclient.Header;
46  import org.apache.commons.httpclient.HeaderElement;
47  import org.apache.commons.httpclient.NameValuePair;
48  import org.apache.commons.httpclient.util.ParameterFormatter;
49  
50  /***
51   * <p>RFC 2965 specific cookie management functions.</p>
52   * 
53   * @author jain.samit@gmail.com (Samit Jain)
54   *
55   * @since 3.1
56   */
57  public class RFC2965Spec extends CookieSpecBase implements CookieVersionSupport {
58  
59      private static final Comparator PATH_COMPOARATOR = new CookiePathComparator();
60      
61      /***
62      * Cookie Response Header  name for cookies processed
63      * by this spec.
64      */
65      public final static String SET_COOKIE2_KEY = "set-cookie2";
66      
67      /***
68      * used for formatting RFC 2956 style cookies
69      */
70      private final ParameterFormatter formatter;
71       
72      /***
73       * Stores the list of attribute handlers
74       */
75      private final List attribHandlerList;
76      
77      /***
78      * Stores attribute name -> attribute handler mappings
79      */
80      private final Map attribHandlerMap;
81  
82      /***
83       * Fallback cookie spec (RFC 2109)
84       */
85      private final CookieSpec rfc2109;
86      
87      /*** 
88       * Default constructor 
89       * */
90      public RFC2965Spec() {
91          super();
92          this.formatter = new ParameterFormatter();
93          this.formatter.setAlwaysUseQuotes(true);
94          this.attribHandlerMap = new HashMap(10);        
95          this.attribHandlerList = new ArrayList(10);
96          this.rfc2109 = new RFC2109Spec();
97          
98          registerAttribHandler(Cookie2.PATH, new Cookie2PathAttributeHandler());
99          registerAttribHandler(Cookie2.DOMAIN, new Cookie2DomainAttributeHandler());
100         registerAttribHandler(Cookie2.PORT, new Cookie2PortAttributeHandler());
101         registerAttribHandler(Cookie2.MAXAGE, new Cookie2MaxageAttributeHandler());
102         registerAttribHandler(Cookie2.SECURE, new CookieSecureAttributeHandler());
103         registerAttribHandler(Cookie2.COMMENT, new CookieCommentAttributeHandler());
104         registerAttribHandler(Cookie2.COMMENTURL, new CookieCommentUrlAttributeHandler());
105         registerAttribHandler(Cookie2.DISCARD, new CookieDiscardAttributeHandler());
106         registerAttribHandler(Cookie2.VERSION, new Cookie2VersionAttributeHandler());
107     }
108 
109     protected void registerAttribHandler(
110             final String name, final CookieAttributeHandler handler) {
111         if (name == null) {
112             throw new IllegalArgumentException("Attribute name may not be null");
113         }
114         if (handler == null) {
115             throw new IllegalArgumentException("Attribute handler may not be null");
116         }
117         if (!this.attribHandlerList.contains(handler)) {
118             this.attribHandlerList.add(handler);
119         }
120         this.attribHandlerMap.put(name, handler);
121     }
122     
123     /***
124      * Finds an attribute handler {@link CookieAttributeHandler} for the
125      * given attribute. Returns <tt>null</tt> if no attribute handler is
126      * found for the specified attribute.
127      *
128      * @param name attribute name. e.g. Domain, Path, etc.
129      * @return an attribute handler or <tt>null</tt>
130      */
131     protected CookieAttributeHandler findAttribHandler(final String name) {
132         return (CookieAttributeHandler) this.attribHandlerMap.get(name);
133     }
134     
135     /***
136      * Gets attribute handler {@link CookieAttributeHandler} for the
137      * given attribute.
138      *
139      * @param name attribute name. e.g. Domain, Path, etc.
140      * @throws IllegalStateException if handler not found for the
141      *          specified attribute.
142      */
143     protected CookieAttributeHandler getAttribHandler(final String name) {
144         CookieAttributeHandler handler = findAttribHandler(name);
145         if (handler == null) {
146             throw new IllegalStateException("Handler not registered for " +
147                                             name + " attribute.");
148         } else {
149             return handler;
150         }
151     }
152 
153     protected Iterator getAttribHandlerIterator() {
154         return this.attribHandlerList.iterator();
155     }
156     
157     /***
158      * Parses the Set-Cookie2 value into an array of <tt>Cookie</tt>s.
159      *
160      * <P>The syntax for the Set-Cookie2 response header is:
161      *
162      * <PRE>
163      * set-cookie      =    "Set-Cookie2:" cookies
164      * cookies         =    1#cookie
165      * cookie          =    NAME "=" VALUE * (";" cookie-av)
166      * NAME            =    attr
167      * VALUE           =    value
168      * cookie-av       =    "Comment" "=" value
169      *                 |    "CommentURL" "=" <"> http_URL <">
170      *                 |    "Discard"
171      *                 |    "Domain" "=" value
172      *                 |    "Max-Age" "=" value
173      *                 |    "Path" "=" value
174      *                 |    "Port" [ "=" <"> portlist <"> ]
175      *                 |    "Secure"
176      *                 |    "Version" "=" 1*DIGIT
177      * portlist        =       1#portnum
178      * portnum         =       1*DIGIT
179      * </PRE>
180      *
181      * @param host the host from which the <tt>Set-Cookie2</tt> value was
182      * received
183      * @param port the port from which the <tt>Set-Cookie2</tt> value was
184      * received
185      * @param path the path from which the <tt>Set-Cookie2</tt> value was
186      * received
187      * @param secure <tt>true</tt> when the <tt>Set-Cookie2</tt> value was
188      * received over secure conection
189      * @param header the <tt>Set-Cookie2</tt> <tt>Header</tt> received from the server
190      * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie2 value
191      * @throws MalformedCookieException if an exception occurs during parsing
192      */
193     public Cookie[] parse(
194             String host, int port, String path, boolean secure, final Header header)
195             throws MalformedCookieException {
196         LOG.trace("enter RFC2965.parse("
197                   + "String, int, String, boolean, Header)");
198 
199         if (header == null) {
200             throw new IllegalArgumentException("Header may not be null.");
201         }
202         if (header.getName() == null) {
203             throw new IllegalArgumentException("Header name may not be null.");
204         }
205 
206         if (header.getName().equalsIgnoreCase(SET_COOKIE2_KEY)) {
207             // parse cookie2 cookies
208             return parse(host, port, path, secure, header.getValue());
209         } else if (header.getName().equalsIgnoreCase(RFC2109Spec.SET_COOKIE_KEY)) {
210             // delegate parsing of old-style cookies to rfc2109Spec
211             return this.rfc2109.parse(host, port, path, secure, header.getValue());
212         } else {
213             throw new MalformedCookieException("Header name is not valid. " +
214                                                "RFC 2965 supports \"set-cookie\" " +
215                                                "and \"set-cookie2\" headers.");
216         }
217     }
218 
219     /***
220      * @see #parse(String, int, String, boolean, org.apache.commons.httpclient.Header)
221      */
222     public Cookie[] parse(String host, int port, String path,
223                           boolean secure, final String header)
224             throws MalformedCookieException {
225         LOG.trace("enter RFC2965Spec.parse("
226                   + "String, int, String, boolean, String)");
227 
228         // before we do anything, lets check validity of arguments
229         if (host == null) {
230             throw new IllegalArgumentException(
231                     "Host of origin may not be null");
232         }
233         if (host.trim().equals("")) {
234             throw new IllegalArgumentException(
235                     "Host of origin may not be blank");
236         }
237         if (port < 0) {
238             throw new IllegalArgumentException("Invalid port: " + port);
239         }
240         if (path == null) {
241             throw new IllegalArgumentException(
242                     "Path of origin may not be null.");
243         }
244         if (header == null) {
245             throw new IllegalArgumentException("Header may not be null.");
246         }
247 
248         if (path.trim().equals("")) {
249             path = PATH_DELIM;
250         }
251         host = getEffectiveHost(host);
252 
253         HeaderElement[] headerElements =
254                 HeaderElement.parseElements(header.toCharArray());
255 
256         List cookies = new LinkedList();
257         for (int i = 0; i < headerElements.length; i++) {
258             HeaderElement headerelement = headerElements[i];
259             Cookie2 cookie = null;
260             try {
261                 cookie = new Cookie2(host,
262                                     headerelement.getName(),
263                                     headerelement.getValue(),
264                                     path,
265                                     null,
266                                     false,
267                                     new int[] {port});
268             } catch (IllegalArgumentException ex) {
269                 throw new MalformedCookieException(ex.getMessage());
270             }
271             NameValuePair[] parameters = headerelement.getParameters();
272             // could be null. In case only a header element and no parameters.
273             if (parameters != null) {
274                 // Eliminate duplicate attribues. The first occurence takes precedence
275                 Map attribmap = new HashMap(parameters.length); 
276                 for (int j = parameters.length - 1; j >= 0; j--) {
277                     NameValuePair param = parameters[j];
278                     attribmap.put(param.getName().toLowerCase(), param);
279                 }
280                 for (Iterator it = attribmap.entrySet().iterator(); it.hasNext(); ) {
281                     Map.Entry entry = (Map.Entry) it.next();
282                     parseAttribute((NameValuePair) entry.getValue(), cookie);
283                 }
284             }
285             cookies.add(cookie);
286             // cycle through the parameters
287         }
288         return (Cookie[]) cookies.toArray(new Cookie[cookies.size()]);
289     }
290 
291     /***
292      * Parse RFC 2965 specific cookie attribute and update the corresponsing
293      * {@link org.apache.commons.httpclient.Cookie} properties.
294      *
295      * @param attribute {@link org.apache.commons.httpclient.NameValuePair} cookie attribute from the
296      * <tt>Set-Cookie2</tt> header.
297      * @param cookie {@link org.apache.commons.httpclient.Cookie} to be updated
298      * @throws MalformedCookieException if an exception occurs during parsing
299      */
300     public void parseAttribute(
301             final NameValuePair attribute, final Cookie cookie)
302             throws MalformedCookieException {
303         if (attribute == null) {
304             throw new IllegalArgumentException("Attribute may not be null.");
305         }
306         if (attribute.getName() == null) {
307             throw new IllegalArgumentException("Attribute Name may not be null.");
308         }
309         if (cookie == null) {
310             throw new IllegalArgumentException("Cookie may not be null.");
311         }
312         final String paramName = attribute.getName().toLowerCase();
313         final String paramValue = attribute.getValue();
314 
315         CookieAttributeHandler handler = findAttribHandler(paramName);
316         if (handler == null) {
317             // ignore unknown attribute-value pairs
318             if (LOG.isDebugEnabled())
319                 LOG.debug("Unrecognized cookie attribute: " +
320                           attribute.toString());
321         } else {
322             handler.parse(cookie, paramValue);
323         }
324     }
325 
326     /***
327      * Performs RFC 2965 compliant {@link org.apache.commons.httpclient.Cookie} validation
328      *
329      * @param host the host from which the {@link org.apache.commons.httpclient.Cookie} was received
330      * @param port the port from which the {@link org.apache.commons.httpclient.Cookie} was received
331      * @param path the path from which the {@link org.apache.commons.httpclient.Cookie} was received
332      * @param secure <tt>true</tt> when the {@link org.apache.commons.httpclient.Cookie} was received using a
333      * secure connection
334      * @param cookie The cookie to validate
335      * @throws MalformedCookieException if an exception occurs during
336      * validation
337      */
338     public void validate(final String host, int port, final String path,
339                          boolean secure, final Cookie cookie)
340             throws MalformedCookieException {
341 
342         LOG.trace("enter RFC2965Spec.validate(String, int, String, "
343                   + "boolean, Cookie)");
344 
345         if (cookie instanceof Cookie2) {
346             if (cookie.getName().indexOf(' ') != -1) {
347                 throw new MalformedCookieException("Cookie name may not contain blanks");
348             }
349             if (cookie.getName().startsWith("$")) {
350                 throw new MalformedCookieException("Cookie name may not start with $");
351             }
352             CookieOrigin origin = new CookieOrigin(getEffectiveHost(host), port, path, secure); 
353             for (Iterator i = getAttribHandlerIterator(); i.hasNext(); ) {
354               CookieAttributeHandler handler = (CookieAttributeHandler) i.next();
355               handler.validate(cookie, origin);
356             }
357         } else {
358             // old-style cookies are validated according to the old rules
359             this.rfc2109.validate(host, port, path, secure, cookie);
360         }
361     }
362 
363     /***
364      * Return <tt>true</tt> if the cookie should be submitted with a request
365      * with given attributes, <tt>false</tt> otherwise.
366      * @param host the host to which the request is being submitted
367      * @param port the port to which the request is being submitted (ignored)
368      * @param path the path to which the request is being submitted
369      * @param secure <tt>true</tt> if the request is using a secure connection
370      * @return true if the cookie matches the criterium
371      */
372     public boolean match(String host, int port, String path,
373                          boolean secure, final Cookie cookie) {
374 
375         LOG.trace("enter RFC2965.match("
376                   + "String, int, String, boolean, Cookie");
377         if (cookie == null) {
378             throw new IllegalArgumentException("Cookie may not be null");
379         }
380         if (cookie instanceof Cookie2) {
381             // check if cookie has expired
382             if (cookie.isPersistent() && cookie.isExpired()) {
383                 return false;
384             }
385             CookieOrigin origin = new CookieOrigin(getEffectiveHost(host), port, path, secure); 
386             for (Iterator i = getAttribHandlerIterator(); i.hasNext(); ) {
387                 CookieAttributeHandler handler = (CookieAttributeHandler) i.next();
388                 if (!handler.match(cookie, origin)) {
389                     return false;
390                 }
391             }
392             return true;
393         } else {
394             // old-style cookies are matched according to the old rules
395             return this.rfc2109.match(host, port, path, secure, cookie);
396         }
397     }
398 
399     private void doFormatCookie2(final Cookie2 cookie, final StringBuffer buffer) {
400         String name = cookie.getName();
401         String value = cookie.getValue();
402         if (value == null) {
403             value = "";
404         }
405         this.formatter.format(buffer, new NameValuePair(name, value));
406         // format domain attribute
407         if (cookie.getDomain() != null && cookie.isDomainAttributeSpecified()) {
408             buffer.append("; ");
409             this.formatter.format(buffer, new NameValuePair("$Domain", cookie.getDomain()));
410         }
411         // format path attribute
412         if ((cookie.getPath() != null) && (cookie.isPathAttributeSpecified())) {
413             buffer.append("; ");
414             this.formatter.format(buffer, new NameValuePair("$Path", cookie.getPath()));
415         }
416         // format port attribute
417         if (cookie.isPortAttributeSpecified()) {
418             String portValue = "";
419             if (!cookie.isPortAttributeBlank()) {
420                 portValue = createPortAttribute(cookie.getPorts());
421             }
422             buffer.append("; ");
423             this.formatter.format(buffer, new NameValuePair("$Port", portValue));
424         }
425     }
426     
427     /***
428      * Return a string suitable for sending in a <tt>"Cookie"</tt> header as
429      * defined in RFC 2965
430      * @param cookie a {@link org.apache.commons.httpclient.Cookie} to be formatted as string
431      * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
432      */
433     public String formatCookie(final Cookie cookie) {
434         LOG.trace("enter RFC2965Spec.formatCookie(Cookie)");
435 
436         if (cookie == null) {
437             throw new IllegalArgumentException("Cookie may not be null");
438         }
439         if (cookie instanceof Cookie2) {
440             Cookie2 cookie2 = (Cookie2) cookie;
441             int version = cookie2.getVersion();
442             final StringBuffer buffer = new StringBuffer();
443             this.formatter.format(buffer, new NameValuePair("$Version", Integer.toString(version)));
444             buffer.append("; ");
445             doFormatCookie2(cookie2, buffer);
446             return buffer.toString();
447         } else {
448             // old-style cookies are formatted according to the old rules
449             return this.rfc2109.formatCookie(cookie);
450         }
451     }
452 
453     /***
454      * Create a RFC 2965 compliant <tt>"Cookie"</tt> header value containing all
455      * {@link org.apache.commons.httpclient.Cookie}s suitable for
456      * sending in a <tt>"Cookie"</tt> header
457      * @param cookies an array of {@link org.apache.commons.httpclient.Cookie}s to be formatted
458      * @return a string suitable for sending in a Cookie header.
459      */
460     public String formatCookies(final Cookie[] cookies) {
461         LOG.trace("enter RFC2965Spec.formatCookieHeader(Cookie[])");
462 
463         if (cookies == null) {
464             throw new IllegalArgumentException("Cookies may not be null");
465         }
466         // check if cookies array contains a set-cookie (old style) cookie
467         boolean hasOldStyleCookie = false;
468         int version = -1;
469         for (int i = 0; i < cookies.length; i++) {
470             Cookie cookie = cookies[i];
471             if (!(cookie instanceof Cookie2)) {
472                 hasOldStyleCookie = true;
473                 break;
474             }
475             if (cookie.getVersion() > version) {
476                 version = cookie.getVersion();
477             }
478         }
479         if (version < 0) {
480             version = 0;
481         }
482         if (hasOldStyleCookie || version < 1) {
483             // delegate old-style cookie formatting to rfc2109Spec
484             return this.rfc2109.formatCookies(cookies);
485         }
486         // Arrange cookies by path
487         Arrays.sort(cookies, PATH_COMPOARATOR);
488         
489         final StringBuffer buffer = new StringBuffer();
490         // format cookie version
491         this.formatter.format(buffer, new NameValuePair("$Version", Integer.toString(version)));
492         for (int i = 0; i < cookies.length; i++) {
493             buffer.append("; ");
494             Cookie2 cookie = (Cookie2) cookies[i];
495             // format cookie attributes
496             doFormatCookie2(cookie, buffer);
497         }
498         return buffer.toString();
499     }
500 
501     /***
502      * Retrieves valid Port attribute value for the given ports array.
503      * e.g. "8000,8001,8002"
504      *
505      * @param ports int array of ports
506      */
507     private String createPortAttribute(int[] ports) {
508         StringBuffer portValue = new StringBuffer();
509         for (int i = 0, len = ports.length; i < len; i++) {
510             if (i > 0) {
511                 portValue.append(",");
512             }
513             portValue.append(ports[i]);
514         }
515         return portValue.toString();
516     }
517 
518     /***
519      * Parses the given Port attribute value (e.g. "8000,8001,8002")
520      * into an array of ports.
521      *
522      * @param portValue port attribute value
523      * @return parsed array of ports
524      * @throws MalformedCookieException if there is a problem in
525      *          parsing due to invalid portValue.
526      */
527     private int[] parsePortAttribute(final String portValue)
528             throws MalformedCookieException {
529         StringTokenizer st = new StringTokenizer(portValue, ",");
530         int[] ports = new int[st.countTokens()];
531         try {
532             int i = 0;
533             while(st.hasMoreTokens()) {
534                 ports[i] = Integer.parseInt(st.nextToken().trim());
535                 if (ports[i] < 0) {
536                   throw new MalformedCookieException ("Invalid Port attribute.");
537                 }
538                 ++i;
539             }
540         } catch (NumberFormatException e) {
541             throw new MalformedCookieException ("Invalid Port "
542                                                 + "attribute: " + e.getMessage());
543         }
544         return ports;
545     }
546 
547     /***
548      * Gets 'effective host name' as defined in RFC 2965.
549      * <p>
550      * If a host name contains no dots, the effective host name is
551      * that name with the string .local appended to it.  Otherwise
552      * the effective host name is the same as the host name.  Note
553      * that all effective host names contain at least one dot.
554      *
555      * @param host host name where cookie is received from or being sent to.
556      * @return
557      */
558     private static String getEffectiveHost(final String host) {
559         String effectiveHost = host.toLowerCase();
560         if (host.indexOf('.') < 0) {
561             effectiveHost += ".local";
562         }
563         return effectiveHost;
564     }
565 
566     /***
567      * Performs domain-match as defined by the RFC2965.
568      * <p>
569      * Host A's name domain-matches host B's if
570      * <ol>
571      *   <ul>their host name strings string-compare equal; or</ul>
572      *   <ul>A is a HDN string and has the form NB, where N is a non-empty
573      *       name string, B has the form .B', and B' is a HDN string.  (So,
574      *       x.y.com domain-matches .Y.com but not Y.com.)</ul>
575      * </ol>
576      *
577      * @param host host name where cookie is received from or being sent to.
578      * @param domain The cookie domain attribute.
579      * @return true if the specified host matches the given domain.
580      */
581     public boolean domainMatch(String host, String domain) {
582         boolean match = host.equals(domain)
583                         || (domain.startsWith(".") && host.endsWith(domain));
584 
585         return match;
586     }
587 
588     /***
589      * Returns <tt>true</tt> if the given port exists in the given
590      * ports list.
591      *
592      * @param port port of host where cookie was received from or being sent to.
593      * @param ports port list
594      * @return true returns <tt>true</tt> if the given port exists in
595      *         the given ports list; <tt>false</tt> otherwise.
596      */
597     private boolean portMatch(int port, int[] ports) {
598         boolean portInList = false;
599         for (int i = 0, len = ports.length; i < len; i++) {
600             if (port == ports[i]) {
601                 portInList = true;
602                 break;
603             }
604         }
605         return portInList;
606     }
607 
608     /***
609      * <tt>"Path"</tt> attribute handler for RFC 2965 cookie spec.
610      */
611     private class Cookie2PathAttributeHandler
612             implements CookieAttributeHandler {
613 
614         /***
615          * Parse cookie path attribute.
616          */
617         public void parse(final Cookie cookie, final String path)
618                 throws MalformedCookieException {
619             if (cookie == null) {
620                 throw new IllegalArgumentException("Cookie may not be null");
621             }
622             if (path == null) {
623                 throw new MalformedCookieException(
624                         "Missing value for path attribute");
625             }
626             if (path.trim().equals("")) {
627                 throw new MalformedCookieException(
628                         "Blank value for path attribute");
629             }
630             cookie.setPath(path);
631             cookie.setPathAttributeSpecified(true);
632         }
633 
634         /***
635          * Validate cookie path attribute. The value for the Path attribute must be a
636          * prefix of the request-URI (case-sensitive matching).
637          */
638         public void validate(final Cookie cookie, final CookieOrigin origin)
639                 throws MalformedCookieException {
640             if (cookie == null) {
641                 throw new IllegalArgumentException("Cookie may not be null");
642             }
643             if (origin == null) {
644                 throw new IllegalArgumentException("Cookie origin may not be null");
645             }
646             String path = origin.getPath();
647             if (path == null) {
648                 throw new IllegalArgumentException(
649                         "Path of origin host may not be null.");
650             }
651             if (cookie.getPath() == null) {
652                 throw new MalformedCookieException("Invalid cookie state: " +
653                                                    "path attribute is null.");
654             }
655             if (path.trim().equals("")) {
656                 path = PATH_DELIM;
657             }
658 
659             if (!pathMatch(path, cookie.getPath())) {
660                 throw new MalformedCookieException(
661                         "Illegal path attribute \"" + cookie.getPath()
662                         + "\". Path of origin: \"" + path + "\"");
663             }
664         }
665 
666         /***
667          * Match cookie path attribute. The value for the Path attribute must be a
668          * prefix of the request-URI (case-sensitive matching).
669          */
670         public boolean match(final Cookie cookie, final CookieOrigin origin) {
671             if (cookie == null) {
672                 throw new IllegalArgumentException("Cookie may not be null");
673             }
674             if (origin == null) {
675                 throw new IllegalArgumentException("Cookie origin may not be null");
676             }
677             String path = origin.getPath();
678             if (cookie.getPath() == null) {
679                 LOG.warn("Invalid cookie state: path attribute is null.");
680                 return false;
681             }
682             if (path.trim().equals("")) {
683                 path = PATH_DELIM;
684             }
685 
686             if (!pathMatch(path, cookie.getPath())) {
687                 return false;
688             }
689             return true;
690         }
691     }
692 
693     /***
694      * <tt>"Domain"</tt> cookie attribute handler for RFC 2965 cookie spec.
695      */
696     private class Cookie2DomainAttributeHandler
697             implements CookieAttributeHandler {
698 
699         /***
700          * Parse cookie domain attribute.
701          */
702         public void parse(final Cookie cookie, String domain)
703                 throws MalformedCookieException {
704             if (cookie == null) {
705                 throw new IllegalArgumentException("Cookie may not be null");
706             }
707             if (domain == null) {
708                 throw new MalformedCookieException(
709                         "Missing value for domain attribute");
710             }
711             if (domain.trim().equals("")) {
712                 throw new MalformedCookieException(
713                         "Blank value for domain attribute");
714             }
715             domain = domain.toLowerCase();
716             if (!domain.startsWith(".")) {
717                 // Per RFC 2965 section 3.2.2
718                 // "... If an explicitly specified value does not start with
719                 // a dot, the user agent supplies a leading dot ..."
720                 // That effectively implies that the domain attribute 
721                 // MAY NOT be an IP address of a host name
722                 domain = "." + domain;
723             }
724             cookie.setDomain(domain);
725             cookie.setDomainAttributeSpecified(true);
726         }
727 
728         /***
729          * Validate cookie domain attribute.
730          */
731         public void validate(final Cookie cookie, final CookieOrigin origin)
732                 throws MalformedCookieException {
733             if (cookie == null) {
734                 throw new IllegalArgumentException("Cookie may not be null");
735             }
736             if (origin == null) {
737                 throw new IllegalArgumentException("Cookie origin may not be null");
738             }
739             String host = origin.getHost().toLowerCase();
740             if (cookie.getDomain() == null) {
741                 throw new MalformedCookieException("Invalid cookie state: " +
742                                                    "domain not specified");
743             }
744             String cookieDomain = cookie.getDomain().toLowerCase();
745 
746             if (cookie.isDomainAttributeSpecified()) {
747                 // Domain attribute must start with a dot
748                 if (!cookieDomain.startsWith(".")) {
749                     throw new MalformedCookieException("Domain attribute \"" +
750                         cookie.getDomain() + "\" violates RFC 2109: domain must start with a dot");
751                 }
752 
753                 // Domain attribute must contain atleast one embedded dot,
754                 // or the value must be equal to .local.
755                 int dotIndex = cookieDomain.indexOf('.', 1);
756                 if (((dotIndex < 0) || (dotIndex == cookieDomain.length() - 1))
757                     && (!cookieDomain.equals(".local"))) {
758                     throw new MalformedCookieException(
759                             "Domain attribute \"" + cookie.getDomain()
760                             + "\" violates RFC 2965: the value contains no embedded dots "
761                             + "and the value is not .local");
762                 }
763 
764                 // The effective host name must domain-match domain attribute.
765                 if (!domainMatch(host, cookieDomain)) {
766                     throw new MalformedCookieException(
767                             "Domain attribute \"" + cookie.getDomain()
768                             + "\" violates RFC 2965: effective host name does not "
769                             + "domain-match domain attribute.");
770                 }
771 
772                 // effective host name minus domain must not contain any dots
773                 String effectiveHostWithoutDomain = host.substring(
774                         0, host.length() - cookieDomain.length());
775                 if (effectiveHostWithoutDomain.indexOf('.') != -1) {
776                     throw new MalformedCookieException("Domain attribute \""
777                                                        + cookie.getDomain() + "\" violates RFC 2965: "
778                                                        + "effective host minus domain may not contain any dots");
779                 }
780             } else {
781                 // Domain was not specified in header. In this case, domain must
782                 // string match request host (case-insensitive).
783                 if (!cookie.getDomain().equals(host)) {
784                     throw new MalformedCookieException("Illegal domain attribute: \""
785                                                        + cookie.getDomain() + "\"."
786                                                        + "Domain of origin: \""
787                                                        + host + "\"");
788                 }
789             }
790         }
791 
792         /***
793          * Match cookie domain attribute.
794          */
795         public boolean match(final Cookie cookie, final CookieOrigin origin) {
796             if (cookie == null) {
797                 throw new IllegalArgumentException("Cookie may not be null");
798             }
799             if (origin == null) {
800                 throw new IllegalArgumentException("Cookie origin may not be null");
801             }
802             String host = origin.getHost().toLowerCase();
803             String cookieDomain = cookie.getDomain();
804 
805             // The effective host name MUST domain-match the Domain
806             // attribute of the cookie.
807             if (!domainMatch(host, cookieDomain)) {
808                 return false;
809             }
810             // effective host name minus domain must not contain any dots
811             String effectiveHostWithoutDomain = host.substring(
812                     0, host.length() - cookieDomain.length());
813             if (effectiveHostWithoutDomain.indexOf('.') != -1) {
814                 return false;
815             }
816             return true;
817         }
818 
819     }
820 
821     /***
822      * <tt>"Port"</tt> cookie attribute handler for RFC 2965 cookie spec.
823      */
824     private class Cookie2PortAttributeHandler
825             implements CookieAttributeHandler {
826 
827         /***
828          * Parse cookie port attribute.
829          */
830         public void parse(final Cookie cookie, final String portValue)
831                 throws MalformedCookieException {
832             if (cookie == null) {
833                 throw new IllegalArgumentException("Cookie may not be null");
834             }
835             if (cookie instanceof Cookie2) {
836                 Cookie2 cookie2 = (Cookie2) cookie;
837                 if ((portValue == null) || (portValue.trim().equals(""))) {
838                     // If the Port attribute is present but has no value, the
839                     // cookie can only be sent to the request-port.
840                     // Since the default port list contains only request-port, we don't
841                     // need to do anything here.
842                     cookie2.setPortAttributeBlank(true);
843                 } else {
844                     int[] ports = parsePortAttribute(portValue);
845                     cookie2.setPorts(ports);
846                 }
847                 cookie2.setPortAttributeSpecified(true);
848             }
849         }
850 
851         /***
852          * Validate cookie port attribute. If the Port attribute was specified
853          * in header, the request port must be in cookie's port list.
854          */
855         public void validate(final Cookie cookie, final CookieOrigin origin)
856                 throws MalformedCookieException {
857             if (cookie == null) {
858                 throw new IllegalArgumentException("Cookie may not be null");
859             }
860             if (origin == null) {
861                 throw new IllegalArgumentException("Cookie origin may not be null");
862             }
863             if (cookie instanceof Cookie2) {
864                 Cookie2 cookie2 = (Cookie2) cookie;
865                 int port = origin.getPort();
866                 if (cookie2.isPortAttributeSpecified()) {
867                     if (!portMatch(port, cookie2.getPorts())) {
868                         throw new MalformedCookieException(
869                                 "Port attribute violates RFC 2965: "
870                                 + "Request port not found in cookie's port list.");
871                     }
872                 }
873             }
874         }
875 
876         /***
877          * Match cookie port attribute. If the Port attribute is not specified
878          * in header, the cookie can be sent to any port. Otherwise, the request port
879          * must be in the cookie's port list.
880          */
881         public boolean match(final Cookie cookie, final CookieOrigin origin) {
882             if (cookie == null) {
883                 throw new IllegalArgumentException("Cookie may not be null");
884             }
885             if (origin == null) {
886                 throw new IllegalArgumentException("Cookie origin may not be null");
887             }
888             if (cookie instanceof Cookie2) {
889                 Cookie2 cookie2 = (Cookie2) cookie;
890                 int port = origin.getPort();
891                 if (cookie2.isPortAttributeSpecified()) {
892                     if (cookie2.getPorts() == null) {
893                         LOG.warn("Invalid cookie state: port not specified");
894                         return false;
895                     }
896                     if (!portMatch(port, cookie2.getPorts())) {
897                         return false;
898                     }
899                 }
900                 return true;
901             } else {
902                 return false;
903             }
904         }
905     }
906 
907   /***
908    * <tt>"Max-age"</tt> cookie attribute handler for RFC 2965 cookie spec.
909    */
910   private class Cookie2MaxageAttributeHandler
911           implements CookieAttributeHandler {
912 
913       /***
914        * Parse cookie max-age attribute.
915        */
916       public void parse(final Cookie cookie, final String value)
917               throws MalformedCookieException {
918           if (cookie == null) {
919               throw new IllegalArgumentException("Cookie may not be null");
920           }
921           if (value == null) {
922               throw new MalformedCookieException(
923                       "Missing value for max-age attribute");
924           }
925           int age = -1;
926           try {
927               age = Integer.parseInt(value);
928           } catch (NumberFormatException e) {
929               age = -1;
930           }
931           if (age < 0) {
932               throw new MalformedCookieException ("Invalid max-age attribute.");
933           }
934           cookie.setExpiryDate(new Date(System.currentTimeMillis() + age * 1000L));
935       }
936 
937       /***
938        * validate cookie max-age attribute.
939        */
940       public void validate(final Cookie cookie, final CookieOrigin origin) {
941       }
942 
943       /***
944        * @see CookieAttributeHandler#match(org.apache.commons.httpclient.Cookie, String)
945        */
946       public boolean match(final Cookie cookie, final CookieOrigin origin) {
947           return true;
948       }
949 
950   }
951 
952   /***
953    * <tt>"Secure"</tt> cookie attribute handler for RFC 2965 cookie spec.
954    */
955   private class CookieSecureAttributeHandler
956           implements CookieAttributeHandler {
957 
958       public void parse(final Cookie cookie, final String secure)
959               throws MalformedCookieException {
960           cookie.setSecure(true);
961       }
962 
963       public void validate(final Cookie cookie, final CookieOrigin origin)
964               throws MalformedCookieException {
965       }
966 
967       public boolean match(final Cookie cookie, final CookieOrigin origin) {
968           if (cookie == null) {
969               throw new IllegalArgumentException("Cookie may not be null");
970           }
971           if (origin == null) {
972               throw new IllegalArgumentException("Cookie origin may not be null");
973           }
974           return cookie.getSecure() == origin.isSecure();
975       }
976       
977   }
978 
979   /***
980    * <tt>"Commant"</tt> cookie attribute handler for RFC 2965 cookie spec.
981    */
982   private class CookieCommentAttributeHandler
983           implements CookieAttributeHandler {
984 
985       public void parse(final Cookie cookie, final String comment)
986               throws MalformedCookieException {
987           cookie.setComment(comment);
988       }
989 
990       public void validate(final Cookie cookie, final CookieOrigin origin)
991               throws MalformedCookieException {
992       }
993 
994       public boolean match(final Cookie cookie, final CookieOrigin origin) {
995           return true;
996       }
997       
998   }
999 
1000   /***
1001    * <tt>"CommantURL"</tt> cookie attribute handler for RFC 2965 cookie spec.
1002    */
1003   private class CookieCommentUrlAttributeHandler
1004           implements CookieAttributeHandler {
1005 
1006       public void parse(final Cookie cookie, final String commenturl)
1007               throws MalformedCookieException {
1008           if (cookie instanceof Cookie2) {
1009               Cookie2 cookie2 = (Cookie2) cookie;
1010               cookie2.setCommentURL(commenturl);
1011           }
1012       }
1013 
1014       public void validate(final Cookie cookie, final CookieOrigin origin)
1015               throws MalformedCookieException {
1016       }
1017 
1018       public boolean match(final Cookie cookie, final CookieOrigin origin) {
1019           return true;
1020       }
1021       
1022   }
1023 
1024   /***
1025    * <tt>"Discard"</tt> cookie attribute handler for RFC 2965 cookie spec.
1026    */
1027   private class CookieDiscardAttributeHandler
1028           implements CookieAttributeHandler {
1029 
1030       public void parse(final Cookie cookie, final String commenturl)
1031               throws MalformedCookieException {
1032           if (cookie instanceof Cookie2) {
1033               Cookie2 cookie2 = (Cookie2) cookie;
1034               cookie2.setDiscard(true);
1035           }
1036       }
1037 
1038       public void validate(final Cookie cookie, final CookieOrigin origin)
1039               throws MalformedCookieException {
1040       }
1041 
1042       public boolean match(final Cookie cookie, final CookieOrigin origin) {
1043           return true;
1044       }
1045       
1046   }
1047 
1048   /***
1049      * <tt>"Version"</tt> cookie attribute handler for RFC 2965 cookie spec.
1050      */
1051     private class Cookie2VersionAttributeHandler
1052             implements CookieAttributeHandler {
1053 
1054         /***
1055          * Parse cookie version attribute.
1056          */
1057         public void parse(final Cookie cookie, final String value)
1058                 throws MalformedCookieException {
1059             if (cookie == null) {
1060                 throw new IllegalArgumentException("Cookie may not be null");
1061             }
1062             if (cookie instanceof Cookie2) {
1063                 Cookie2 cookie2 = (Cookie2) cookie;
1064                 if (value == null) {
1065                     throw new MalformedCookieException(
1066                             "Missing value for version attribute");
1067                 }
1068                 int version = -1;
1069                 try {
1070                     version = Integer.parseInt(value);
1071                 } catch (NumberFormatException e) {
1072                     version = -1;
1073                 }
1074                 if (version < 0) {
1075                     throw new MalformedCookieException("Invalid cookie version.");
1076                 }
1077                 cookie2.setVersion(version);
1078                 cookie2.setVersionAttributeSpecified(true);
1079             }
1080         }
1081 
1082         /***
1083          * validate cookie version attribute. Version attribute is REQUIRED.
1084          */
1085         public void validate(final Cookie cookie, final CookieOrigin origin)
1086                 throws MalformedCookieException {
1087             if (cookie == null) {
1088                 throw new IllegalArgumentException("Cookie may not be null");
1089             }
1090             if (cookie instanceof Cookie2) {
1091                 Cookie2 cookie2 = (Cookie2) cookie;
1092                 if (!cookie2.isVersionAttributeSpecified()) {
1093                     throw new MalformedCookieException(
1094                             "Violates RFC 2965. Version attribute is required.");
1095                 }
1096             }
1097         }
1098 
1099         public boolean match(final Cookie cookie, final CookieOrigin origin) {
1100             return true;
1101         }
1102 
1103     }
1104 
1105     public int getVersion() {
1106         return 1;
1107     }
1108 
1109     public Header getVersionHeader() {
1110         ParameterFormatter formatter = new ParameterFormatter();
1111         StringBuffer buffer = new StringBuffer();
1112         formatter.format(buffer, new NameValuePair("$Version",
1113                 Integer.toString(getVersion())));
1114         return new Header("Cookie2", buffer.toString(), true);
1115     }
1116     
1117 }
1118