View Javadoc

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