1 /* 2 * Copyright 2005-2008 Sun Microsystems, Inc. All Rights Reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Sun designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Sun in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, 22 * CA 95054 USA or visit www.sun.com if you need additional information or 23 * have any questions. 24 */ 25 26 package java.net; 27 28 import java.util.Map; 29 import java.util.List; 30 import java.util.Collections; 31 import java.util.Comparator; 32 import java.io.IOException; 33 34 /** 35 * CookieManager provides a concrete implementation of {@link CookieHandler}, 36 * which separates the storage of cookies from the policy surrounding accepting 37 * and rejecting cookies. A CookieManager is initialized with a {@link CookieStore} 38 * which manages storage, and a {@link CookiePolicy} object, which makes 39 * policy decisions on cookie acceptance/rejection. 40 * 41 * <p> The HTTP cookie management in java.net package looks like: 42 * <blockquote> 43 * <pre> 44 * use 45 * CookieHandler <------- HttpURLConnection 46 * ^ 47 * | impl 48 * | use 49 * CookieManager -------> CookiePolicy 50 * | use 51 * |--------> HttpCookie 52 * | ^ 53 * | | use 54 * | use | 55 * |--------> CookieStore 56 * ^ 57 * | impl 58 * | 59 * Internal in-memory implementation 60 * </pre> 61 * <ul> 62 * <li> 63 * CookieHandler is at the core of cookie management. User can call 64 * CookieHandler.setDefault to set a concrete CookieHanlder implementation 65 * to be used. 66 * </li> 67 * <li> 68 * CookiePolicy.shouldAccept will be called by CookieManager.put to see whether 69 * or not one cookie should be accepted and put into cookie store. User can use 70 * any of three pre-defined CookiePolicy, namely ACCEPT_ALL, ACCEPT_NONE and 71 * ACCEPT_ORIGINAL_SERVER, or user can define his own CookiePolicy implementation 72 * and tell CookieManager to use it. 73 * </li> 74 * <li> 75 * CookieStore is the place where any accepted HTTP cookie is stored in. 76 * If not specified when created, a CookieManager instance will use an internal 77 * in-memory implementation. Or user can implements one and tell CookieManager 78 * to use it. 79 * </li> 80 * <li> 81 * Currently, only CookieStore.add(URI, HttpCookie) and CookieStore.get(URI) 82 * are used by CookieManager. Others are for completeness and might be needed 83 * by a more sophisticated CookieStore implementation, e.g. a NetscapeCookieSotre. 84 * </li> 85 * </ul> 86 * </blockquote> 87 * 88 * <p>There're various ways user can hook up his own HTTP cookie management behavior, e.g. 89 * <blockquote> 90 * <ul> 91 * <li>Use CookieHandler.setDefault to set a brand new {@link CookieHandler} implementation 92 * <li>Let CookieManager be the default {@link CookieHandler} implementation, 93 * but implement user's own {@link CookieStore} and {@link CookiePolicy} 94 * and tell default CookieManager to use them: 95 * <blockquote><pre> 96 * // this should be done at the beginning of an HTTP session 97 * CookieHandler.setDefault(new CookieManager(new MyCookieStore(), new MyCookiePolicy())); 98 * </pre></blockquote> 99 * <li>Let CookieManager be the default {@link CookieHandler} implementation, but 100 * use customized {@link CookiePolicy}: 101 * <blockquote><pre> 102 * // this should be done at the beginning of an HTTP session 103 * CookieHandler.setDefault(new CookieManager()); 104 * // this can be done at any point of an HTTP session 105 * ((CookieManager)CookieHandler.getDefault()).setCookiePolicy(new MyCookiePolicy()); 106 * </pre></blockquote> 107 * </ul> 108 * </blockquote> 109 * 110 * <p>The implementation conforms to <a href="http://www.ietf.org/rfc/rfc2965.txt">RFC 2965</a>, section 3.3. 111 * 112 * @see CookiePolicy 113 * @author Edward Wang 114 * @since 1.6 115 */ 116 public class CookieManager extends CookieHandler 117 { 118 /* ---------------- Fields -------------- */ 119 120 private CookiePolicy policyCallback; 121 122 123 private CookieStore cookieJar = null; 124 125 126 /* ---------------- Ctors -------------- */ 127 128 /** 129 * Create a new cookie manager. 130 * 131 * <p>This constructor will create new cookie manager with default 132 * cookie store and accept policy. The effect is same as 133 * <tt>CookieManager(null, null)</tt>. 134 */ 135 public CookieManager() { 136 this(null, null); 137 } 138 139 140 /** 141 * Create a new cookie manager with specified cookie store and cookie policy. 142 * 143 * @param store a <tt>CookieStore</tt> to be used by cookie manager. 144 * if <tt>null</tt>, cookie manager will use a default one, 145 * which is an in-memory CookieStore implmentation. 146 * @param cookiePolicy a <tt>CookiePolicy</tt> instance 147 * to be used by cookie manager as policy callback. 148 * if <tt>null</tt>, ACCEPT_ORIGINAL_SERVER will 149 * be used. 150 */ 151 public CookieManager(CookieStore store, 152 CookiePolicy cookiePolicy) 153 { 154 // use default cookie policy if not specify one 155 policyCallback = (cookiePolicy == null) ? CookiePolicy.ACCEPT_ORIGINAL_SERVER 156 : cookiePolicy; 157 158 // if not specify CookieStore to use, use default one 159 if (store == null) { 160 cookieJar = new InMemoryCookieStore(); 161 } else { 162 cookieJar = store; 163 } 164 } 165 166 167 /* ---------------- Public operations -------------- */ 168 169 /** 170 * To set the cookie policy of this cookie manager. 171 * 172 * <p> A instance of <tt>CookieManager</tt> will have 173 * cookie policy ACCEPT_ORIGINAL_SERVER by default. Users always 174 * can call this method to set another cookie policy. 175 * 176 * @param cookiePolicy the cookie policy. Can be <tt>null</tt>, which 177 * has no effects on current cookie policy. 178 */ 179 public void setCookiePolicy(CookiePolicy cookiePolicy) { 180 if (cookiePolicy != null) policyCallback = cookiePolicy; 181 } 182 183 184 /** 185 * To retrieve current cookie store. 186 * 187 * @return the cookie store currently used by cookie manager. 188 */ 189 public CookieStore getCookieStore() { 190 return cookieJar; 191 } 192 193 194 public Map<String, List<String>> 195 get(URI uri, Map<String, List<String>> requestHeaders) 196 throws IOException 197 { 198 // pre-condition check 199 if (uri == null || requestHeaders == null) { 200 throw new IllegalArgumentException("Argument is null"); 201 } 202 203 Map<String, List<String>> cookieMap = 204 new java.util.HashMap<String, List<String>>(); 205 // if there's no default CookieStore, no way for us to get any cookie 206 if (cookieJar == null) 207 return Collections.unmodifiableMap(cookieMap); 208 209 boolean secureLink = "https".equalsIgnoreCase(uri.getScheme()); 210 List<HttpCookie> cookies = new java.util.ArrayList<HttpCookie>(); 211 String path = uri.getPath(); 212 if (path == null || path.isEmpty()) { 213 path = "/"; 214 } 215 for (HttpCookie cookie : cookieJar.get(uri)) { 216 // apply path-matches rule (RFC 2965 sec. 3.3.4) 217 // and check for the possible "secure" tag (i.e. don't send 218 // 'secure' cookies over unsecure links) 219 if (pathMatches(path, cookie.getPath()) && 220 (secureLink || !cookie.getSecure())) { 221 // Let's check the authorize port list if it exists 222 String ports = cookie.getPortlist(); 223 if (ports != null && !ports.isEmpty()) { 224 int port = uri.getPort(); 225 if (port == -1) { 226 port = "https".equals(uri.getScheme()) ? 443 : 80; 227 } 228 if (isInPortList(ports, port)) { 229 cookies.add(cookie); 230 } 231 } else { 232 cookies.add(cookie); 233 } 234 } 235 } 236 237 // apply sort rule (RFC 2965 sec. 3.3.4) 238 List<String> cookieHeader = sortByPath(cookies); 239 240 cookieMap.put("Cookie", cookieHeader); 241 return Collections.unmodifiableMap(cookieMap); 242 } 243 244 245 public void 246 put(URI uri, Map<String, List<String>> responseHeaders) 247 throws IOException 248 { 249 // pre-condition check 250 if (uri == null || responseHeaders == null) { 251 throw new IllegalArgumentException("Argument is null"); 252 } 253 254 255 // if there's no default CookieStore, no need to remember any cookie 256 if (cookieJar == null) 257 return; 258 259 for (String headerKey : responseHeaders.keySet()) { 260 // RFC 2965 3.2.2, key must be 'Set-Cookie2' 261 // we also accept 'Set-Cookie' here for backward compatibility 262 if (headerKey == null 263 || !(headerKey.equalsIgnoreCase("Set-Cookie2") 264 || headerKey.equalsIgnoreCase("Set-Cookie") 265 ) 266 ) 267 { 268 continue; 269 } 270 271 for (String headerValue : responseHeaders.get(headerKey)) { 272 try { 273 List<HttpCookie> cookies = HttpCookie.parse(headerValue); 274 for (HttpCookie cookie : cookies) { 275 if (cookie.getPath() == null) { 276 // If no path is specified, then by default 277 // the path is the directory of the page/doc 278 String path = uri.getPath(); 279 if (!path.endsWith("/")) { 280 int i = path.lastIndexOf("/"); 281 if (i > 0) { 282 path = path.substring(0, i + 1); 283 } else { 284 path = "/"; 285 } 286 } 287 cookie.setPath(path); 288 } 289 290 // As per RFC 2965, section 3.3.1: 291 // Domain Defaults to the effective request-host. (Note that because 292 // there is no dot at the beginning of effective request-host, 293 // the default Domain can only domain-match itself.) 294 if (cookie.getDomain() == null) { 295 cookie.setDomain(uri.getHost()); 296 } 297 String ports = cookie.getPortlist(); 298 if (ports != null) { 299 int port = uri.getPort(); 300 if (port == -1) { 301 port = "https".equals(uri.getScheme()) ? 443 : 80; 302 } 303 if (ports.isEmpty()) { 304 // Empty port list means this should be restricted 305 // to the incoming URI port 306 cookie.setPortlist("" + port ); 307 if (shouldAcceptInternal(uri, cookie)) { 308 cookieJar.add(uri, cookie); 309 } 310 } else { 311 // Only store cookies with a port list 312 // IF the URI port is in that list, as per 313 // RFC 2965 section 3.3.2 314 if (isInPortList(ports, port) && 315 shouldAcceptInternal(uri, cookie)) { 316 cookieJar.add(uri, cookie); 317 } 318 } 319 } else { 320 if (shouldAcceptInternal(uri, cookie)) { 321 cookieJar.add(uri, cookie); 322 } 323 } 324 } 325 } catch (IllegalArgumentException e) { 326 // invalid set-cookie header string 327 // no-op 328 } 329 } 330 } 331 } 332 333 334 /* ---------------- Private operations -------------- */ 335 336 // to determine whether or not accept this cookie 337 private boolean shouldAcceptInternal(URI uri, HttpCookie cookie) { 338 try { 339 return policyCallback.shouldAccept(uri, cookie); 340 } catch (Exception ignored) { // pretect against malicious callback 341 return false; 342 } 343 } 344 345 346 static private boolean isInPortList(String lst, int port) { 347 int i = lst.indexOf(","); 348 int val = -1; 349 while (i > 0) { 350 try { 351 val = Integer.parseInt(lst.substring(0, i)); 352 if (val == port) { 353 return true; 354 } 355 } catch (NumberFormatException numberFormatException) { 356 } 357 lst = lst.substring(i+1); 358 i = lst.indexOf(","); 359 } 360 if (!lst.isEmpty()) { 361 try { 362 val = Integer.parseInt(lst); 363 if (val == port) { 364 return true; 365 } 366 } catch (NumberFormatException numberFormatException) { 367 } 368 } 369 return false; 370 } 371 372 /* 373 * path-matches algorithm, as defined by RFC 2965 374 */ 375 private boolean pathMatches(String path, String pathToMatchWith) { 376 if (path == pathToMatchWith) 377 return true; 378 if (path == null || pathToMatchWith == null) 379 return false; 380 if (path.startsWith(pathToMatchWith)) 381 return true; 382 383 return false; 384 } 385 386 387 /* 388 * sort cookies with respect to their path: those with more specific Path attributes 389 * precede those with less specific, as defined in RFC 2965 sec. 3.3.4 390 */ 391 private List<String> sortByPath(List<HttpCookie> cookies) { 392 Collections.sort(cookies, new CookiePathComparator()); 393 394 List<String> cookieHeader = new java.util.ArrayList<String>(); 395 for (HttpCookie cookie : cookies) { 396 // Netscape cookie spec and RFC 2965 have different format of Cookie 397 // header; RFC 2965 requires a leading $Version="1" string while Netscape 398 // does not. 399 // The workaround here is to add a $Version="1" string in advance 400 if (cookies.indexOf(cookie) == 0 && cookie.getVersion() > 0) { 401 cookieHeader.add("$Version=\"1\""); 402 } 403 404 cookieHeader.add(cookie.toString()); 405 } 406 return cookieHeader; 407 } 408 409 410 static class CookiePathComparator implements Comparator<HttpCookie> { 411 public int compare(HttpCookie c1, HttpCookie c2) { 412 if (c1 == c2) return 0; 413 if (c1 == null) return -1; 414 if (c2 == null) return 1; 415 416 // path rule only applies to the cookies with same name 417 if (!c1.getName().equals(c2.getName())) return 0; 418 419 // those with more specific Path attributes precede those with less specific 420 if (c1.getPath().startsWith(c2.getPath())) 421 return -1; 422 else if (c2.getPath().startsWith(c1.getPath())) 423 return 1; 424 else 425 return 0; 426 } 427 } 428 }