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