View Javadoc

    1   /*
    2    * $HeadURL: https://svn.apache.org/repos/asf/jakarta/httpcomponents/oac.hc3x/tags/HTTPCLIENT_3_1/src/java/org/apache/commons/httpclient/cookie/RFC2965Spec.java $
    3    * $Revision: 507134 $
    4    * $Date: 2007-02-13 19:18:05 +0100 (Tue, 13 Feb 2007) $
    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