Java Source Code: org.apache.commons.httpclient.cookie.RFC2965Spec


   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:}