View Javadoc
1 /* 2 * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java,v 1.16 2003/06/12 19:12:16 olegk Exp $ 3 * $Revision: 1.16 $ 4 * $Date: 2003/06/12 19:12:16 $ 5 * 6 * ==================================================================== 7 * 8 * The Apache Software License, Version 1.1 9 * 10 * Copyright (c) 2002-2003 The Apache Software Foundation. All rights 11 * reserved. 12 * 13 * Redistribution and use in source and binary forms, with or without 14 * modification, are permitted provided that the following conditions 15 * are met: 16 * 17 * 1. Redistributions of source code must retain the above copyright 18 * notice, this list of conditions and the following disclaimer. 19 * 20 * 2. Redistributions in binary form must reproduce the above copyright 21 * notice, this list of conditions and the following disclaimer in 22 * the documentation and/or other materials provided with the 23 * distribution. 24 * 25 * 3. The end-user documentation included with the redistribution, if 26 * any, must include the following acknowlegement: 27 * "This product includes software developed by the 28 * Apache Software Foundation (http://www.apache.org/)." 29 * Alternately, this acknowlegement may appear in the software itself, 30 * if and wherever such third-party acknowlegements normally appear. 31 * 32 * 4. The names "The Jakarta Project", "Commons", and "Apache Software 33 * Foundation" must not be used to endorse or promote products derived 34 * from this software without prior written permission. For written 35 * permission, please contact apache@apache.org. 36 * 37 * 5. Products derived from this software may not be called "Apache" 38 * nor may "Apache" appear in their names without prior written 39 * permission of the Apache Group. 40 * 41 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED 42 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 43 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 44 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR 45 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 46 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 47 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 48 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 49 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 50 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 51 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 52 * SUCH DAMAGE. 53 * ==================================================================== 54 * 55 * This software consists of voluntary contributions made by many 56 * individuals on behalf of the Apache Software Foundation. For more 57 * information on the Apache Software Foundation, please see 58 * <http://www.apache.org/>. 59 * 60 * [Additional notices, if required by prior licensing conditions] 61 * 62 */ 63 64 package org.apache.commons.httpclient.cookie; 65 66 import java.util.Date; 67 import java.util.LinkedList; 68 import java.util.List; 69 70 import org.apache.commons.httpclient.Cookie; 71 import org.apache.commons.httpclient.Header; 72 import org.apache.commons.httpclient.HeaderElement; 73 import org.apache.commons.httpclient.HttpException; 74 import org.apache.commons.httpclient.NameValuePair; 75 import org.apache.commons.httpclient.util.DateParseException; 76 import org.apache.commons.httpclient.util.DateParser; 77 import org.apache.commons.logging.Log; 78 import org.apache.commons.logging.LogFactory; 79 80 /*** 81 * 82 * Cookie management functions shared by all specification. 83 * 84 * @author B.C. Holmes 85 * @author <a href="mailto:jericho@thinkfree.com">Park, Sung-Gu</a> 86 * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a> 87 * @author Rod Waldhoff 88 * @author dIon Gillard 89 * @author Sean C. Sullivan 90 * @author <a href="mailto:JEvans@Cyveillance.com">John Evans</a> 91 * @author Marc A. Saegesser 92 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a> 93 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> 94 * 95 * @since 2.0 96 */ 97 public class CookieSpecBase implements CookieSpec { 98 99 /*** Log object */ 100 protected static final Log LOG = LogFactory.getLog(CookieSpec.class); 101 102 /*** Default constructor */ 103 public CookieSpecBase() { 104 super(); 105 } 106 107 108 /*** 109 * Parses the Set-Cookie value into an array of <tt>Cookie</tt>s. 110 * 111 * <P>The syntax for the Set-Cookie response header is: 112 * 113 * <PRE> 114 * set-cookie = "Set-Cookie:" cookies 115 * cookies = 1#cookie 116 * cookie = NAME "=" VALUE * (";" cookie-av) 117 * NAME = attr 118 * VALUE = value 119 * cookie-av = "Comment" "=" value 120 * | "Domain" "=" value 121 * | "Max-Age" "=" value 122 * | "Path" "=" value 123 * | "Secure" 124 * | "Version" "=" 1*DIGIT 125 * </PRE> 126 * 127 * @param host the host from which the <tt>Set-Cookie</tt> value was 128 * received 129 * @param port the port from which the <tt>Set-Cookie</tt> value was 130 * received 131 * @param path the path from which the <tt>Set-Cookie</tt> value was 132 * received 133 * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> value was 134 * received over secure conection 135 * @param header the <tt>Set-Cookie</tt> received from the server 136 * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie value 137 * @throws MalformedCookieException if an exception occurs during parsing 138 */ 139 public Cookie[] parse(String host, int port, String path, 140 boolean secure, final String header) 141 throws MalformedCookieException { 142 143 LOG.trace("enter CookieSpecBase.parse(" 144 + "String, port, path, boolean, Header)"); 145 146 if (host == null) { 147 throw new IllegalArgumentException( 148 "Host of origin may not be null"); 149 } 150 if (host.trim().equals("")) { 151 throw new IllegalArgumentException( 152 "Host of origin may not be blank"); 153 } 154 if (port < 0) { 155 throw new IllegalArgumentException("Invalid port: " + port); 156 } 157 if (path == null) { 158 throw new IllegalArgumentException( 159 "Path of origin may not be null."); 160 } 161 if (header == null) { 162 throw new IllegalArgumentException("Header may not be null."); 163 } 164 165 if (path.trim().equals("")) { 166 path = PATH_DELIM; 167 } 168 host = host.toLowerCase(); 169 170 HeaderElement[] headerElements = null; 171 try { 172 headerElements = HeaderElement.parse(header); 173 } catch (HttpException e) { 174 throw new MalformedCookieException(e.getMessage()); 175 } 176 177 String defaultPath = path; 178 int lastSlashIndex = defaultPath.lastIndexOf(PATH_DELIM); 179 if (lastSlashIndex >= 0) { 180 if (lastSlashIndex == 0) { 181 //Do not remove the very first slash 182 lastSlashIndex = 1; 183 } 184 defaultPath = defaultPath.substring(0, lastSlashIndex); 185 } 186 187 Cookie[] cookies = new Cookie[headerElements.length]; 188 189 for (int i = 0; i < headerElements.length; i++) { 190 191 HeaderElement headerelement = headerElements[i]; 192 Cookie cookie = new Cookie(host, 193 headerelement.getName(), 194 headerelement.getValue(), 195 defaultPath, 196 null, 197 false); 198 199 // cycle through the parameters 200 NameValuePair[] parameters = headerelement.getParameters(); 201 // could be null. In case only a header element and no parameters. 202 if (parameters != null) { 203 204 for (int j = 0; j < parameters.length; j++) { 205 parseAttribute(parameters[j], cookie); 206 } 207 } 208 cookies[i] = cookie; 209 } 210 return cookies; 211 } 212 213 214 /*** 215 * Parse the <tt>"Set-Cookie"</tt> {@link Header} into an array of {@link 216 * Cookie}s. 217 * 218 * <P>The syntax for the Set-Cookie response header is: 219 * 220 * <PRE> 221 * set-cookie = "Set-Cookie:" cookies 222 * cookies = 1#cookie 223 * cookie = NAME "=" VALUE * (";" cookie-av) 224 * NAME = attr 225 * VALUE = value 226 * cookie-av = "Comment" "=" value 227 * | "Domain" "=" value 228 * | "Max-Age" "=" value 229 * | "Path" "=" value 230 * | "Secure" 231 * | "Version" "=" 1*DIGIT 232 * </PRE> 233 * 234 * @param host the host from which the <tt>Set-Cookie</tt> header was 235 * received 236 * @param port the port from which the <tt>Set-Cookie</tt> header was 237 * received 238 * @param path the path from which the <tt>Set-Cookie</tt> header was 239 * received 240 * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> header was 241 * received over secure conection 242 * @param header the <tt>Set-Cookie</tt> received from the server 243 * @return an array of <tt>Cookie</tt>s parsed from the <tt>"Set-Cookie" 244 * </tt> header 245 * @throws MalformedCookieException if an exception occurs during parsing 246 */ 247 public Cookie[] parse( 248 String host, int port, String path, boolean secure, final Header header) 249 throws MalformedCookieException { 250 251 LOG.trace("enter CookieSpecBase.parse(" 252 + "String, port, path, boolean, String)"); 253 if (header == null) { 254 throw new IllegalArgumentException("Header may not be null."); 255 } 256 return parse(host, port, path, secure, header.getValue()); 257 } 258 259 260 /*** 261 * Parse the cookie attribute and update the corresponsing {@link Cookie} 262 * properties. 263 * 264 * @param attribute {@link HeaderElement} cookie attribute from the 265 * <tt>Set- Cookie</tt> 266 * @param cookie {@link Cookie} to be updated 267 * @throws MalformedCookieException if an exception occurs during parsing 268 */ 269 270 public void parseAttribute( 271 final NameValuePair attribute, final Cookie cookie) 272 throws MalformedCookieException { 273 274 if (attribute == null) { 275 throw new IllegalArgumentException("Attribute may not be null."); 276 } 277 if (cookie == null) { 278 throw new IllegalArgumentException("Cookie may not be null."); 279 } 280 final String paramName = attribute.getName().toLowerCase(); 281 String paramValue = attribute.getValue(); 282 283 if (paramName.equals("path")) { 284 285 if ((paramValue == null) || (paramValue.trim().equals(""))) { 286 paramValue = "/"; 287 } 288 cookie.setPath(paramValue); 289 cookie.setPathAttributeSpecified(true); 290 291 } else if (paramName.equals("domain")) { 292 293 if (paramValue == null) { 294 throw new MalformedCookieException( 295 "Missing value for domain attribute"); 296 } 297 if (paramValue.trim().equals("")) { 298 throw new MalformedCookieException( 299 "Blank value for domain attribute"); 300 } 301 cookie.setDomain(paramValue); 302 cookie.setDomainAttributeSpecified(true); 303 304 } else if (paramName.equals("max-age")) { 305 306 if (paramValue == null) { 307 throw new MalformedCookieException( 308 "Missing value for max-age attribute"); 309 } 310 int age; 311 try { 312 age = Integer.parseInt(paramValue); 313 } catch (NumberFormatException e) { 314 throw new MalformedCookieException ("Invalid max-age " 315 + "attribute: " + e.getMessage()); 316 } 317 cookie.setExpiryDate( 318 new Date(System.currentTimeMillis() + age * 1000L)); 319 320 } else if (paramName.equals("secure")) { 321 322 cookie.setSecure(true); 323 324 } else if (paramName.equals("comment")) { 325 326 cookie.setComment(paramValue); 327 328 } else if (paramName.equals("expires")) { 329 330 if (paramValue == null) { 331 throw new MalformedCookieException( 332 "Missing value for expires attribute"); 333 } 334 // trim single quotes around expiry if present 335 // see http://nagoya.apache.org/bugzilla/show_bug.cgi?id=5279 336 if (paramValue.length() > 1 337 && paramValue.startsWith("'") 338 && paramValue.endsWith("'")) { 339 paramValue 340 = paramValue.substring (1, paramValue.length() - 1); 341 } 342 343 try { 344 cookie.setExpiryDate(DateParser.parseDate(paramValue)); 345 } catch (DateParseException dpe) { 346 LOG.debug("Error parsing cookie date", dpe); 347 throw new MalformedCookieException( 348 "Unable to parse expiration date parameter: " 349 + paramValue); 350 } 351 } else { 352 if (LOG.isDebugEnabled()) { 353 LOG.debug("Unrecognized cookie attribute: " 354 + attribute.toString()); 355 } 356 } 357 } 358 359 360 /*** 361 * Performs most common {@link Cookie} validation 362 * 363 * @param host the host from which the {@link Cookie} was received 364 * @param port the port from which the {@link Cookie} was received 365 * @param path the path from which the {@link Cookie} was received 366 * @param secure <tt>true</tt> when the {@link Cookie} was received using a 367 * secure connection 368 * @param cookie The cookie to validate. 369 * @throws MalformedCookieException if an exception occurs during 370 * validation 371 */ 372 373 public void validate(String host, int port, String path, 374 boolean secure, final Cookie cookie) 375 throws MalformedCookieException { 376 377 LOG.trace("enter CookieSpecBase.validate(" 378 + "String, port, path, boolean, Cookie)"); 379 if (host == null) { 380 throw new IllegalArgumentException( 381 "Host of origin may not be null"); 382 } 383 if (host.trim().equals("")) { 384 throw new IllegalArgumentException( 385 "Host of origin may not be blank"); 386 } 387 if (port < 0) { 388 throw new IllegalArgumentException("Invalid port: " + port); 389 } 390 if (path == null) { 391 throw new IllegalArgumentException( 392 "Path of origin may not be null."); 393 } 394 if (path.trim().equals("")) { 395 path = PATH_DELIM; 396 } 397 host = host.toLowerCase(); 398 // check version 399 if (cookie.getVersion() < 0) { 400 throw new MalformedCookieException ("Illegal version number " 401 + cookie.getValue()); 402 } 403 404 // security check... we musn't allow the server to give us an 405 // invalid domain scope 406 407 // Validate the cookies domain attribute. NOTE: Domains without 408 // any dots are allowed to support hosts on private LANs that don't 409 // have DNS names. Since they have no dots, to domain-match the 410 // request-host and domain must be identical for the cookie to sent 411 // back to the origin-server. 412 if (host.indexOf(".") >= 0) { 413 // Not required to have at least two dots. RFC 2965. 414 // A Set-Cookie2 with Domain=ajax.com will be accepted. 415 416 // domain must match host 417 if (!host.endsWith(cookie.getDomain())) { 418 throw new MalformedCookieException( 419 "Illegal domain attribute \"" + cookie.getDomain() 420 + "\". Domain of origin: \"" + host + "\""); 421 } 422 } else { 423 if (!host.equals(cookie.getDomain())) { 424 throw new MalformedCookieException( 425 "Illegal domain attribute \"" + cookie.getDomain() 426 + "\". Domain of origin: \"" + host + "\""); 427 } 428 } 429 430 // another security check... we musn't allow the server to give us a 431 // cookie that doesn't match this path 432 433 if (!path.startsWith(cookie.getPath())) { 434 throw new MalformedCookieException( 435 "Illegal path attribute \"" + cookie.getPath() 436 + "\". Path of origin: \"" + path + "\""); 437 } 438 } 439 440 441 /*** 442 * Return <tt>true</tt> if the cookie should be submitted with a request 443 * with given attributes, <tt>false</tt> otherwise. 444 * @param host the host to which the request is being submitted 445 * @param port the port to which the request is being submitted (ignored) 446 * @param path the path to which the request is being submitted 447 * @param secure <tt>true</tt> if the request is using a secure connection 448 * @param cookie {@link Cookie} to be matched 449 * @return true if the cookie matches the criterium 450 */ 451 452 public boolean match(String host, int port, String path, 453 boolean secure, final Cookie cookie) { 454 455 LOG.trace("enter CookieSpecBase.match(" 456 + "String, int, String, boolean, Cookie"); 457 458 if (host == null) { 459 throw new IllegalArgumentException( 460 "Host of origin may not be null"); 461 } 462 if (host.trim().equals("")) { 463 throw new IllegalArgumentException( 464 "Host of origin may not be blank"); 465 } 466 if (port < 0) { 467 throw new IllegalArgumentException("Invalid port: " + port); 468 } 469 if (path == null) { 470 throw new IllegalArgumentException( 471 "Path of origin may not be null."); 472 } 473 if (cookie == null) { 474 throw new IllegalArgumentException("Cookie may not be null"); 475 } 476 if (path.trim().equals("")) { 477 path = PATH_DELIM; 478 } 479 host = host.toLowerCase(); 480 if (cookie.getDomain() == null) { 481 LOG.warn("Invalid cookie state: domain not specified"); 482 return false; 483 } 484 if (cookie.getPath() == null) { 485 LOG.warn("Invalid cookie state: path not specified"); 486 return false; 487 } 488 489 return 490 // only add the cookie if it hasn't yet expired 491 (cookie.getExpiryDate() == null 492 || cookie.getExpiryDate().after(new Date())) 493 // and the domain pattern matches 494 && (domainMatch(host, cookie.getDomain())) 495 // and the path is null or matching 496 && (pathMatch(path, cookie.getPath())) 497 // and if the secure flag is set, only if the request is 498 // actually secure 499 && (cookie.getSecure() ? secure : true); 500 } 501 502 /*** 503 * Performs a domain-match as described in RFC2109. 504 * @param host The host to check. 505 * @param domain The domain. 506 * @return true if the specified host matches the given domain. 507 */ 508 private static boolean domainMatch(String host, String domain) { 509 boolean match = host.equals(domain) 510 || (domain.startsWith(".") && host.endsWith(domain)); 511 512 return match; 513 } 514 515 /*** 516 * Performs a path-match slightly smarter than a straight-forward startsWith 517 * check. 518 * @param path The path to check. 519 * @param topmostPath The path to check against. 520 * @return true if the paths match 521 */ 522 private static boolean pathMatch( 523 final String path, final String topmostPath) { 524 525 boolean match = path.startsWith (topmostPath); 526 527 // if there is a match and these values are not exactly the same we have 528 // to make sure we're not matcing "/foobar" and "/foo" 529 if (match && path.length() != topmostPath.length()) { 530 if (!topmostPath.endsWith(PATH_DELIM)) { 531 match = (path.charAt(topmostPath.length()) == PATH_DELIM_CHAR); 532 } 533 } 534 return match; 535 } 536 537 /*** 538 * Return an array of {@link Cookie}s that should be submitted with a 539 * request with given attributes, <tt>false</tt> otherwise. 540 * @param host the host to which the request is being submitted 541 * @param port the port to which the request is being submitted (currently 542 * ignored) 543 * @param path the path to which the request is being submitted 544 * @param secure <tt>true</tt> if the request is using a secure protocol 545 * @param cookies an array of <tt>Cookie</tt>s to be matched 546 * @return an array of <tt>Cookie</tt>s matching the criterium 547 */ 548 549 public Cookie[] match(String host, int port, String path, 550 boolean secure, final Cookie cookies[]) { 551 552 LOG.trace("enter CookieSpecBase.match(" 553 + "String, int, String, boolean, Cookie[])"); 554 555 if (host == null) { 556 throw new IllegalArgumentException( 557 "Host of origin may not be null"); 558 } 559 if (host.trim().equals("")) { 560 throw new IllegalArgumentException( 561 "Host of origin may not be blank"); 562 } 563 if (port < 0) { 564 throw new IllegalArgumentException("Invalid port: " + port); 565 } 566 if (path == null) { 567 throw new IllegalArgumentException( 568 "Path of origin may not be null."); 569 } 570 if (cookies == null) { 571 throw new IllegalArgumentException("Cookie array may not be null"); 572 } 573 if (path.trim().equals("")) { 574 path = PATH_DELIM; 575 } 576 host = host.toLowerCase(); 577 578 if (cookies.length <= 0) { 579 return null; 580 } 581 List matching = new LinkedList(); 582 for (int i = 0; i < cookies.length; i++) { 583 if (match(host, port, path, secure, cookies[i])) { 584 addInPathOrder(matching, cookies[i]); 585 } 586 } 587 return (Cookie[]) matching.toArray(new Cookie[matching.size()]); 588 } 589 590 591 /*** 592 * Adds the given cookie into the given list in descending path order. That 593 * is, more specific path to least specific paths. This may not be the 594 * fastest algorythm, but it'll work OK for the small number of cookies 595 * we're generally dealing with. 596 * 597 * @param list - the list to add the cookie to 598 * @param addCookie - the Cookie to add to list 599 */ 600 private static void addInPathOrder(List list, Cookie addCookie) { 601 int i = 0; 602 603 for (i = 0; i < list.size(); i++) { 604 Cookie c = (Cookie) list.get(i); 605 if (addCookie.compare(addCookie, c) > 0) { 606 break; 607 } 608 } 609 list.add(i, addCookie); 610 } 611 612 /*** 613 * Return a string suitable for sending in a <tt>"Cookie"</tt> header 614 * @param cookie a {@link Cookie} to be formatted as string 615 * @return a string suitable for sending in a <tt>"Cookie"</tt> header. 616 */ 617 public String formatCookie(Cookie cookie) { 618 LOG.trace("enter CookieSpecBase.formatCookie(Cookie)"); 619 if (cookie == null) { 620 throw new IllegalArgumentException("Cookie may not be null"); 621 } 622 StringBuffer buf = new StringBuffer(); 623 buf.append(cookie.getName()); 624 buf.append("="); 625 String s = cookie.getValue(); 626 if (s != null) { 627 buf.append(s); 628 }; 629 return buf.toString(); 630 } 631 632 /*** 633 * Create a <tt>"Cookie"</tt> header value containing all {@link Cookie}s in 634 * <i>cookies</i> suitable for sending in a <tt>"Cookie"</tt> header 635 * @param cookies an array of {@link Cookie}s to be formatted 636 * @return a string suitable for sending in a Cookie header. 637 * @throws IllegalArgumentException if an input parameter is illegal 638 */ 639 640 public String formatCookies(Cookie[] cookies) 641 throws IllegalArgumentException { 642 LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])"); 643 if (cookies == null) { 644 throw new IllegalArgumentException("Cookie array may not be null"); 645 } 646 if (cookies.length == 0) { 647 throw new IllegalArgumentException("Cookie array may not be empty"); 648 } 649 650 StringBuffer buffer = new StringBuffer(); 651 for (int i = 0; i < cookies.length; i++) { 652 if (i > 0) { 653 buffer.append("; "); 654 } 655 buffer.append(formatCookie(cookies[i])); 656 } 657 return buffer.toString(); 658 } 659 660 661 /*** 662 * Create a <tt>"Cookie"</tt> {@link Header} containing all {@link Cookie}s 663 * in <i>cookies</i>. 664 * @param cookies an array of {@link Cookie}s to be formatted as a <tt>" 665 * Cookie"</tt> header 666 * @return a <tt>"Cookie"</tt> {@link Header}. 667 */ 668 public Header formatCookieHeader(Cookie[] cookies) { 669 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])"); 670 return new Header("Cookie", formatCookies(cookies)); 671 } 672 673 674 /*** 675 * Create a <tt>"Cookie"</tt> {@link Header} containing the {@link Cookie}. 676 * @param cookie <tt>Cookie</tt>s to be formatted as a <tt>Cookie</tt> 677 * header 678 * @return a Cookie header. 679 */ 680 public Header formatCookieHeader(Cookie cookie) { 681 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)"); 682 return new Header("Cookie", formatCookie(cookie)); 683 } 684 685 }

This page was automatically generated by Maven