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 * */ RFC2965Spec()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 registerAttribHandler( final String name, final CookieAttributeHandler handler)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 */ findAttribHandler(final String name)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 */ getAttribHandler(final String name)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 getAttribHandlerIterator()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 */ parse( String host, int port, String path, boolean secure, final Header header)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 */ parse(String host, int port, String path, boolean secure, final String header)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 */ parseAttribute( final NameValuePair attribute, final Cookie cookie)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 */ validate(final String host, int port, final String path, boolean secure, final Cookie cookie)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 */ match(String host, int port, String path, boolean secure, final Cookie cookie)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 doFormatCookie2(final Cookie2 cookie, final StringBuffer buffer)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 */ formatCookie(final Cookie cookie)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 */ formatCookies(final Cookie[] cookies)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 */ createPortAttribute(int[] ports)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 */ parsePortAttribute(final String portValue)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 */ getEffectiveHost(final String host)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 */ domainMatch(String host, String domain)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 */ portMatch(int port, int[] ports)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 */ parse(final Cookie cookie, final String path)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 */ validate(final Cookie cookie, final CookieOrigin origin)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 */ match(final Cookie cookie, final CookieOrigin origin)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 */ parse(final Cookie cookie, String domain)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 */ validate(final Cookie cookie, final CookieOrigin origin)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 */ match(final Cookie cookie, final CookieOrigin origin)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 */ parse(final Cookie cookie, final String portValue)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 */ validate(final Cookie cookie, final CookieOrigin origin)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 */ match(final Cookie cookie, final CookieOrigin origin)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 */ parse(final Cookie cookie, final String value)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 */ validate(final Cookie cookie, final CookieOrigin origin)940 public void validate(final Cookie cookie, final CookieOrigin origin) { 941 } 942 943 /** 944 * @see CookieAttributeHandler#match(org.apache.commons.httpclient.Cookie, String) 945 */ match(final Cookie cookie, final CookieOrigin origin)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 parse(final Cookie cookie, final String secure)958 public void parse(final Cookie cookie, final String secure) 959 throws MalformedCookieException { 960 cookie.setSecure(true); 961 } 962 validate(final Cookie cookie, final CookieOrigin origin)963 public void validate(final Cookie cookie, final CookieOrigin origin) 964 throws MalformedCookieException { 965 } 966 match(final Cookie cookie, final CookieOrigin origin)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 parse(final Cookie cookie, final String comment)985 public void parse(final Cookie cookie, final String comment) 986 throws MalformedCookieException { 987 cookie.setComment(comment); 988 } 989 validate(final Cookie cookie, final CookieOrigin origin)990 public void validate(final Cookie cookie, final CookieOrigin origin) 991 throws MalformedCookieException { 992 } 993 match(final Cookie cookie, final CookieOrigin origin)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 parse(final Cookie cookie, final String commenturl)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 validate(final Cookie cookie, final CookieOrigin origin)1014 public void validate(final Cookie cookie, final CookieOrigin origin) 1015 throws MalformedCookieException { 1016 } 1017 match(final Cookie cookie, final CookieOrigin origin)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 parse(final Cookie cookie, final String commenturl)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 validate(final Cookie cookie, final CookieOrigin origin)1038 public void validate(final Cookie cookie, final CookieOrigin origin) 1039 throws MalformedCookieException { 1040 } 1041 match(final Cookie cookie, final CookieOrigin origin)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 */ parse(final Cookie cookie, final String value)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 */ validate(final Cookie cookie, final CookieOrigin origin)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 match(final Cookie cookie, final CookieOrigin origin)1099 public boolean match(final Cookie cookie, final CookieOrigin origin) { 1100 return true; 1101 } 1102 1103 } 1104 getVersion()1105 public int getVersion() { 1106 return 1; 1107 } 1108 getVersionHeader()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 1119