View Javadoc
1 /* 2 * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java,v 1.14 2003/04/17 03:00:31 mbecke Exp $ 3 * $Revision: 1.14 $ 4 * $Date: 2003/04/17 03:00:31 $ 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 defaultPath = defaultPath.substring(0, lastSlashIndex); 181 } 182 183 Cookie[] cookies = new Cookie[headerElements.length]; 184 185 for (int i = 0; i < headerElements.length; i++) { 186 187 HeaderElement headerelement = headerElements[i]; 188 Cookie cookie = new Cookie(host, 189 headerelement.getName(), 190 headerelement.getValue(), 191 defaultPath, 192 null, 193 false); 194 195 // cycle through the parameters 196 NameValuePair[] parameters = headerelement.getParameters(); 197 // could be null. In case only a header element and no parameters. 198 if (parameters != null) { 199 200 for (int j = 0; j < parameters.length; j++) { 201 parseAttribute(parameters[j], cookie); 202 } 203 } 204 cookies[i] = cookie; 205 } 206 return cookies; 207 } 208 209 210 /*** 211 * Parse the <tt>"Set-Cookie"</tt> {@link Header} into an array of {@link 212 * Cookie}s. 213 * 214 * <P>The syntax for the Set-Cookie response header is: 215 * 216 * <PRE> 217 * set-cookie = "Set-Cookie:" cookies 218 * cookies = 1#cookie 219 * cookie = NAME "=" VALUE * (";" cookie-av) 220 * NAME = attr 221 * VALUE = value 222 * cookie-av = "Comment" "=" value 223 * | "Domain" "=" value 224 * | "Max-Age" "=" value 225 * | "Path" "=" value 226 * | "Secure" 227 * | "Version" "=" 1*DIGIT 228 * </PRE> 229 * 230 * @param host the host from which the <tt>Set-Cookie</tt> header was 231 * received 232 * @param port the port from which the <tt>Set-Cookie</tt> header was 233 * received 234 * @param path the path from which the <tt>Set-Cookie</tt> header was 235 * received 236 * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> header was 237 * received over secure conection 238 * @param header the <tt>Set-Cookie</tt> received from the server 239 * @return an array of <tt>Cookie</tt>s parsed from the <tt>"Set-Cookie" 240 * </tt> header 241 * @throws MalformedCookieException if an exception occurs during parsing 242 */ 243 public Cookie[] parse( 244 String host, int port, String path, boolean secure, final Header header) 245 throws MalformedCookieException { 246 247 LOG.trace("enter CookieSpecBase.parse(" 248 + "String, port, path, boolean, String)"); 249 if (header == null) { 250 throw new IllegalArgumentException("Header may not be null."); 251 } 252 return parse(host, port, path, secure, header.getValue()); 253 } 254 255 256 /*** 257 * Parse the cookie attribute and update the corresponsing {@link Cookie} 258 * properties. 259 * 260 * @param attribute {@link HeaderElement} cookie attribute from the 261 * <tt>Set- Cookie</tt> 262 * @param cookie {@link Cookie} to be updated 263 * @throws MalformedCookieException if an exception occurs during parsing 264 */ 265 266 public void parseAttribute( 267 final NameValuePair attribute, final Cookie cookie) 268 throws MalformedCookieException { 269 270 if (attribute == null) { 271 throw new IllegalArgumentException("Attribute may not be null."); 272 } 273 if (cookie == null) { 274 throw new IllegalArgumentException("Cookie may not be null."); 275 } 276 final String paramName = attribute.getName().toLowerCase(); 277 String paramValue = attribute.getValue(); 278 279 if (paramName.equals("path")) { 280 281 if (paramValue == null) { 282 throw new MalformedCookieException( 283 "Missing value for path attribute"); 284 } 285 if (paramValue.trim().equals("")) { 286 throw new MalformedCookieException( 287 "Blank value for path attribute"); 288 } 289 cookie.setPath(paramValue); 290 cookie.setPathAttributeSpecified(true); 291 292 } else if (paramName.equals("domain")) { 293 294 if (paramValue == null) { 295 throw new MalformedCookieException( 296 "Missing value for domain attribute"); 297 } 298 if (paramValue.trim().equals("")) { 299 throw new MalformedCookieException( 300 "Blank value for domain attribute"); 301 } 302 cookie.setDomain(paramValue); 303 cookie.setDomainAttributeSpecified(true); 304 305 } else if (paramName.equals("max-age")) { 306 307 if (paramValue == null) { 308 throw new MalformedCookieException( 309 "Missing value for max-age attribute"); 310 } 311 int age; 312 try { 313 age = Integer.parseInt(paramValue); 314 } catch (NumberFormatException e) { 315 throw new MalformedCookieException ("Invalid max-age " 316 + "attribute: " + e.getMessage()); 317 } 318 cookie.setExpiryDate( 319 new Date(System.currentTimeMillis() + age * 1000L)); 320 321 } else if (paramName.equals("secure")) { 322 323 cookie.setSecure(true); 324 325 } else if (paramName.equals("comment")) { 326 327 cookie.setComment(paramValue); 328 329 } else if (paramName.equals("expires")) { 330 331 if (paramValue == null) { 332 throw new MalformedCookieException( 333 "Missing value for expires attribute"); 334 } 335 // trim single quotes around expiry if present 336 // see http://nagoya.apache.org/bugzilla/show_bug.cgi?id=5279 337 if (paramValue.length() > 1 338 && paramValue.startsWith("'") 339 && paramValue.endsWith("'")) { 340 paramValue 341 = paramValue.substring (1, paramValue.length() - 1); 342 } 343 344 try { 345 cookie.setExpiryDate(DateParser.parseDate(paramValue)); 346 } catch (DateParseException dpe) { 347 LOG.debug("Error parsing cookie date", dpe); 348 throw new MalformedCookieException( 349 "Unable to parse expiration date parameter: " 350 + paramValue); 351 } 352 } else { 353 if (LOG.isDebugEnabled()) { 354 LOG.debug("Unrecognized cookie attribute: " 355 + attribute.toString()); 356 } 357 } 358 } 359 360 361 /*** 362 * Performs most common {@link Cookie} validation 363 * 364 * @param host the host from which the {@link Cookie} was received 365 * @param port the port from which the {@link Cookie} was received 366 * @param path the path from which the {@link Cookie} was received 367 * @param secure <tt>true</tt> when the {@link Cookie} was received using a 368 * secure connection 369 * @param cookie The cookie to validate. 370 * @throws MalformedCookieException if an exception occurs during 371 * validation 372 */ 373 374 public void validate(String host, int port, String path, 375 boolean secure, final Cookie cookie) 376 throws MalformedCookieException { 377 378 LOG.trace("enter CookieSpecBase.validate(" 379 + "String, port, path, boolean, Cookie)"); 380 if (host == null) { 381 throw new IllegalArgumentException( 382 "Host of origin may not be null"); 383 } 384 if (host.trim().equals("")) { 385 throw new IllegalArgumentException( 386 "Host of origin may not be blank"); 387 } 388 if (port < 0) { 389 throw new IllegalArgumentException("Invalid port: " + port); 390 } 391 if (path == null) { 392 throw new IllegalArgumentException( 393 "Path of origin may not be null."); 394 } 395 if (path.trim().equals("")) { 396 path = PATH_DELIM; 397 } 398 host = host.toLowerCase(); 399 // check version 400 if (cookie.getVersion() < 0) { 401 throw new MalformedCookieException ("Illegal version number " 402 + cookie.getValue()); 403 } 404 405 // security check... we musn't allow the server to give us an 406 // invalid domain scope 407 408 // Validate the cookies domain attribute. NOTE: Domains without 409 // any dots are allowed to support hosts on private LANs that don't 410 // have DNS names. Since they have no dots, to domain-match the 411 // request-host and domain must be identical for the cookie to sent 412 // back to the origin-server. 413 if (host.indexOf(".") >= 0) { 414 // Not required to have at least two dots. RFC 2965. 415 // A Set-Cookie2 with Domain=ajax.com will be accepted. 416 417 // domain must match host 418 if (!host.endsWith(cookie.getDomain())) { 419 throw new MalformedCookieException( 420 "Illegal domain attribute \"" + cookie.getDomain() 421 + "\". Domain of origin: \"" + host + "\""); 422 } 423 } else { 424 if (!host.equals(cookie.getDomain())) { 425 throw new MalformedCookieException( 426 "Illegal domain attribute \"" + cookie.getDomain() 427 + "\". Domain of origin: \"" + host + "\""); 428 } 429 } 430 431 // another security check... we musn't allow the server to give us a 432 // cookie that doesn't match this path 433 434 if (!path.startsWith(cookie.getPath())) { 435 throw new MalformedCookieException( 436 "Illegal path attribute \"" + cookie.getPath() 437 + "\". Path of origin: \"" + path + "\""); 438 } 439 } 440 441 442 /*** 443 * Return <tt>true</tt> if the cookie should be submitted with a request 444 * with given attributes, <tt>false</tt> otherwise. 445 * @param host the host to which the request is being submitted 446 * @param port the port to which the request is being submitted (ignored) 447 * @param path the path to which the request is being submitted 448 * @param secure <tt>true</tt> if the request is using a secure connection 449 * @param cookie {@link Cookie} to be matched 450 * @return true if the cookie matches the criterium 451 */ 452 453 public boolean match(String host, int port, String path, 454 boolean secure, final Cookie cookie) { 455 456 LOG.trace("enter CookieSpecBase.match(" 457 + "String, int, String, boolean, Cookie"); 458 459 if (host == null) { 460 throw new IllegalArgumentException( 461 "Host of origin may not be null"); 462 } 463 if (host.trim().equals("")) { 464 throw new IllegalArgumentException( 465 "Host of origin may not be blank"); 466 } 467 if (port < 0) { 468 throw new IllegalArgumentException("Invalid port: " + port); 469 } 470 if (path == null) { 471 throw new IllegalArgumentException( 472 "Path of origin may not be null."); 473 } 474 if (cookie == null) { 475 throw new IllegalArgumentException("Cookie may not be null"); 476 } 477 if (path.trim().equals("")) { 478 path = PATH_DELIM; 479 } 480 host = host.toLowerCase(); 481 if (cookie.getDomain() == null) { 482 LOG.warn("Invalid cookie state: domain not specified"); 483 return false; 484 } 485 if (cookie.getPath() == null) { 486 LOG.warn("Invalid cookie state: path not specified"); 487 return false; 488 } 489 490 return 491 // only add the cookie if it hasn't yet expired 492 (cookie.getExpiryDate() == null 493 || cookie.getExpiryDate().after(new Date())) 494 // and the domain pattern matches 495 && (domainMatch(host, cookie.getDomain())) 496 // and the path is null or matching 497 && (pathMatch(path, cookie.getPath())) 498 // and if the secure flag is set, only if the request is 499 // actually secure 500 && (cookie.getSecure() ? secure : true); 501 } 502 503 /*** 504 * Performs a domain-match as described in RFC2109. 505 * @param host The host to check. 506 * @param domain The domain. 507 * @return true if the specified host matches the given domain. 508 */ 509 private static boolean domainMatch(String host, String domain) { 510 boolean match = host.equals(domain) 511 || (domain.startsWith(".") && host.endsWith(domain)); 512 513 return match; 514 } 515 516 /*** 517 * Performs a path-match slightly smarter than a straight-forward startsWith 518 * check. 519 * @param path The path to check. 520 * @param topmostPath The path to check against. 521 * @return true if the paths match 522 */ 523 private static boolean pathMatch( 524 final String path, final String topmostPath) { 525 526 boolean match = path.startsWith (topmostPath); 527 528 // if there is a match and these values are not exactly the same we have 529 // to make sure we're not matcing "/foobar" and "/foo" 530 if (match && path.length() != topmostPath.length()) { 531 if (!topmostPath.endsWith(PATH_DELIM)) { 532 match = (path.charAt(topmostPath.length()) == PATH_DELIM_CHAR); 533 } 534 } 535 return match; 536 } 537 538 /*** 539 * Return an array of {@link Cookie}s that should be submitted with a 540 * request with given attributes, <tt>false</tt> otherwise. 541 * @param host the host to which the request is being submitted 542 * @param port the port to which the request is being submitted (currently 543 * ignored) 544 * @param path the path to which the request is being submitted 545 * @param secure <tt>true</tt> if the request is using a secure protocol 546 * @param cookies an array of <tt>Cookie</tt>s to be matched 547 * @return an array of <tt>Cookie</tt>s matching the criterium 548 */ 549 550 public Cookie[] match(String host, int port, String path, 551 boolean secure, final Cookie cookies[]) { 552 553 LOG.trace("enter CookieSpecBase.match(" 554 + "String, int, String, boolean, Cookie[])"); 555 556 if (host == null) { 557 throw new IllegalArgumentException( 558 "Host of origin may not be null"); 559 } 560 if (host.trim().equals("")) { 561 throw new IllegalArgumentException( 562 "Host of origin may not be blank"); 563 } 564 if (port < 0) { 565 throw new IllegalArgumentException("Invalid port: " + port); 566 } 567 if (path == null) { 568 throw new IllegalArgumentException( 569 "Path of origin may not be null."); 570 } 571 if (cookies == null) { 572 throw new IllegalArgumentException("Cookie array may not be null"); 573 } 574 if (path.trim().equals("")) { 575 path = PATH_DELIM; 576 } 577 host = host.toLowerCase(); 578 579 if (cookies.length <= 0) { 580 return null; 581 } 582 List matching = new LinkedList(); 583 for (int i = 0; i < cookies.length; i++) { 584 if (match(host, port, path, secure, cookies[i])) { 585 addInPathOrder(matching, cookies[i]); 586 } 587 } 588 return (Cookie[]) matching.toArray(new Cookie[matching.size()]); 589 } 590 591 592 /*** 593 * Adds the given cookie into the given list in descending path order. That 594 * is, more specific path to least specific paths. This may not be the 595 * fastest algorythm, but it'll work OK for the small number of cookies 596 * we're generally dealing with. 597 * 598 * @param list - the list to add the cookie to 599 * @param addCookie - the Cookie to add to list 600 */ 601 private static void addInPathOrder(List list, Cookie addCookie) { 602 int i = 0; 603 604 for (i = 0; i < list.size(); i++) { 605 Cookie c = (Cookie) list.get(i); 606 if (addCookie.compare(addCookie, c) > 0) { 607 break; 608 } 609 } 610 list.add(i, addCookie); 611 } 612 613 /*** 614 * Return a string suitable for sending in a <tt>"Cookie"</tt> header 615 * @param cookie a {@link Cookie} to be formatted as string 616 * @return a string suitable for sending in a <tt>"Cookie"</tt> header. 617 */ 618 public String formatCookie(Cookie cookie) { 619 LOG.trace("enter CookieSpecBase.formatCookie(Cookie)"); 620 if (cookie == null) { 621 throw new IllegalArgumentException("Cookie may not be null"); 622 } 623 StringBuffer buf = new StringBuffer(); 624 buf.append(cookie.getName()); 625 buf.append("="); 626 String s = cookie.getValue(); 627 if (s != null) { 628 buf.append(s); 629 }; 630 return buf.toString(); 631 } 632 633 /*** 634 * Create a <tt>"Cookie"</tt> header value containing all {@link Cookie}s in 635 * <i>cookies</i> suitable for sending in a <tt>"Cookie"</tt> header 636 * @param cookies an array of {@link Cookie}s to be formatted 637 * @return a string suitable for sending in a Cookie header. 638 * @throws IllegalArgumentException if an input parameter is illegal 639 */ 640 641 public String formatCookies(Cookie[] cookies) 642 throws IllegalArgumentException { 643 LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])"); 644 if (cookies == null) { 645 throw new IllegalArgumentException("Cookie array may not be null"); 646 } 647 if (cookies.length == 0) { 648 throw new IllegalArgumentException("Cookie array may not be empty"); 649 } 650 651 StringBuffer buffer = new StringBuffer(); 652 for (int i = 0; i < cookies.length; i++) { 653 if (i > 0) { 654 buffer.append("; "); 655 } 656 buffer.append(formatCookie(cookies[i])); 657 } 658 return buffer.toString(); 659 } 660 661 662 /*** 663 * Create a <tt>"Cookie"</tt> {@link Header} containing all {@link Cookie}s 664 * in <i>cookies</i>. 665 * @param cookies an array of {@link Cookie}s to be formatted as a <tt>" 666 * Cookie"</tt> header 667 * @return a <tt>"Cookie"</tt> {@link Header}. 668 */ 669 public Header formatCookieHeader(Cookie[] cookies) { 670 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])"); 671 return new Header("Cookie", formatCookies(cookies)); 672 } 673 674 675 /*** 676 * Create a <tt>"Cookie"</tt> {@link Header} containing the {@link Cookie}. 677 * @param cookie <tt>Cookie</tt>s to be formatted as a <tt>Cookie</tt> 678 * header 679 * @return a Cookie header. 680 */ 681 public Header formatCookieHeader(Cookie cookie) { 682 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)"); 683 return new Header("Cookie", formatCookie(cookie)); 684 } 685 686 }

This page was automatically generated by Maven