1 module hunt.http.client.InMemoryCookieStore; 2 3 import hunt.http.client.CookieStore; 4 5 import hunt.http.Cookie; 6 import hunt.collection; 7 import hunt.Exceptions; 8 import hunt.logging; 9 import hunt.util.Comparator; 10 import hunt.net.util.HttpURI; 11 12 import core.sync.rwmutex; 13 14 import std.algorithm; 15 import std.range; 16 import std.string; 17 18 /** 19 * A simple in-memory CookieStore implementation 20 * 21 * @author Edward Wang 22 */ 23 class InMemoryCookieStore : CookieStore { 24 25 // the in-memory representation of cookies 26 private List!HttpCookie cookieJar; 27 28 // the cookies are indexed by its domain and associated uri (if present) 29 // CAUTION: when a cookie removed from main data structure (i.e. cookieJar), 30 // it won't be cleared in domainIndex & uriIndex. Double-check the 31 // presence of cookie when retrieve one form index store. 32 // private Map!(string, List!HttpCookie) domainIndex; 33 private Map!(string, List!HttpCookie) uriIndex; 34 35 private ReadWriteMutex lock; 36 37 this() { 38 cookieJar = new ArrayList!HttpCookie(); 39 uriIndex = new HashMap!(string, List!HttpCookie)(); 40 this.lock = new ReadWriteMutex(); 41 } 42 43 /** 44 * Add one cookie into cookie store. 45 */ 46 void add(HttpURI uri, HttpCookie cookie) { 47 if (cookie is null) { 48 throw new NullPointerException("cookie is null"); 49 } 50 51 if (uri is null) { 52 throw new NullPointerException("uri is null"); 53 } 54 55 lock.writer().lock(); 56 scope(exit) lock.writer().unlock(); 57 58 // remove the ole cookie if there has had one 59 cookieJar.remove(cookie); 60 61 // add new cookie if it has a non-zero max-age 62 if (!cookie.isExpired()) { 63 cookieJar.add(cookie); 64 // and add it to domain index 65 // string domain = cookie.getDomain(); 66 // if (!domain.empty()) { 67 // addIndex(domainIndex, domain, cookie); 68 // } 69 70 // add it to uri index, too 71 addIndex(uriIndex, getEffectiveURI(uri), cookie); 72 } 73 } 74 75 76 /** 77 * Get all cookies, which: 78 * 1) given uri domain-matches with, or, associated with 79 * given uri when added to the cookie store. 80 * 3) not expired. 81 * See RFC 2965 sec. 3.3.4 for more detail. 82 */ 83 HttpCookie[] get(string uri) { 84 // argument can't be null 85 if (uri.empty()) { 86 throw new NullPointerException("uri is null"); 87 } 88 89 HttpURI httpUri = new HttpURI(uri); 90 bool secureLink = icmp("https", httpUri.getScheme()) == 0; 91 92 lock.reader().lock(); 93 scope(exit) lock.reader().unlock(); 94 95 List!(HttpCookie) cookies = new ArrayList!HttpCookie(); 96 try { 97 // check domainIndex first 98 // getInternal1(cookies, domainIndex, httpUri.getHost(), secureLink); 99 // check uriIndex then 100 getInternal2(cookies, uriIndex, getEffectiveURI(httpUri), secureLink); 101 } catch(Exception ex ) { 102 debug warning(ex.msg); 103 } 104 105 return cookies.toArray(); 106 } 107 108 /** 109 * Get all cookies in cookie store, except those have expired 110 */ 111 HttpCookie[] getCookies() { 112 lock.reader().lock(); 113 scope(exit) lock.reader().unlock(); 114 115 try { 116 HttpCookie[] expiredCookies; 117 foreach(HttpCookie c; cookieJar) { 118 if(c.isExpired()) { 119 expiredCookies ~= c; 120 } 121 } 122 foreach(HttpCookie c; expiredCookies) { 123 cookieJar.remove(c); 124 } 125 } catch(Exception ex) { 126 debug warning(ex.msg); 127 } 128 129 return cookieJar.toArray[]; 130 } 131 132 /** 133 * Get all URIs, which are associated with at least one cookie 134 * of this cookie store. 135 */ 136 string[] getURIs() { 137 lock.reader().lock(); 138 scope(exit) lock.reader().unlock(); 139 return uriIndex.byKey().array(); 140 } 141 142 143 /** 144 * Remove a cookie from store 145 */ 146 bool remove(string uri, HttpCookie cookie) { 147 // argument can't be null 148 if (cookie is null) { 149 throw new NullPointerException("cookie is null"); 150 } 151 lock.writer().lock(); 152 scope(exit) lock.writer().unlock(); 153 154 return cookieJar.remove(cookie); 155 } 156 157 158 /** 159 * Remove all cookies in this cookie store. 160 */ 161 bool removeAll() { 162 lock.writer().lock(); 163 scope(exit) lock.writer().unlock(); 164 165 if (cookieJar.isEmpty()) { 166 return false; 167 } 168 cookieJar.clear(); 169 // domainIndex.clear(); 170 uriIndex.clear(); 171 172 return true; 173 } 174 175 /** 176 * Removes all of {@link Cookie cookies} in this HTTP state 177 * that have expired by the specified {@link java.util.Date date}. 178 * 179 * @return true if any cookies were purged. 180 * 181 * @see Cookie#isExpired(time) 182 */ 183 bool clearExpired() { 184 lock.writer().lock(); 185 scope(exit) lock.writer().unlock(); 186 187 scope Cookie[] tempCookies; 188 foreach (Cookie it; cookieJar) { 189 if (it.isExpired()) { 190 tempCookies ~= it; 191 } 192 } 193 194 foreach(Cookie c; tempCookies) { 195 cookieJar.remove(c); 196 } 197 198 199 return tempCookies.length > 0; 200 } 201 /* ---------------- Private operations -------------- */ 202 203 204 /* 205 * This is almost the same as HttpCookie.domainMatches except for 206 * one difference: It won't reject cookies when the 'H' part of the 207 * domain contains a dot ('.'). 208 * I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com 209 * and the cookie domain is .domain.com, then it should be rejected. 210 * However that's not how the real world works. Browsers don't reject and 211 * some sites, like yahoo.com do actually expect these cookies to be 212 * passed along. 213 * And should be used for 'old' style cookies (aka Netscape type of cookies) 214 */ 215 // private bool netscapeDomainMatches(String domain, String host) 216 // { 217 // if (domain is null || host is null) { 218 // return false; 219 // } 220 221 // // if there's no embedded dot in domain and domain is not .local 222 // bool isLocalDomain = ".local".equalsIgnoreCase(domain); 223 // int embeddedDotInDomain = domain.indexOf('.'); 224 // if (embeddedDotInDomain == 0) { 225 // embeddedDotInDomain = domain.indexOf('.', 1); 226 // } 227 // if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) { 228 // return false; 229 // } 230 231 // // if the host name contains no dot and the domain name is .local 232 // int firstDotInHost = host.indexOf('.'); 233 // if (firstDotInHost == -1 && isLocalDomain) { 234 // return true; 235 // } 236 237 // int domainLength = domain.length(); 238 // int lengthDiff = host.length() - domainLength; 239 // if (lengthDiff == 0) { 240 // // if the host name and the domain name are just string-compare euqal 241 // return host.equalsIgnoreCase(domain); 242 // } else if (lengthDiff > 0) { 243 // // need to check H & D component 244 // String H = host.substring(0, lengthDiff); 245 // String D = host.substring(lengthDiff); 246 247 // return (D.equalsIgnoreCase(domain)); 248 // } else if (lengthDiff == -1) { 249 // // if domain is actually .host 250 // return (domain.charAt(0) == '.' && 251 // host.equalsIgnoreCase(domain.substring(1))); 252 // } 253 254 // return false; 255 // } 256 257 // private void getInternal1(List!(HttpCookie) cookies, Map!(String, List!(HttpCookie)) cookieIndex, 258 // String host, bool secureLink) { 259 // // Use a separate list to handle cookies that need to be removed so 260 // // that there is no conflict with iterators. 261 // ArrayList!(HttpCookie) toRemove = new ArrayList<>(); 262 // for (Map.Entry!(String, List!(HttpCookie)) entry : cookieIndex.entrySet()) { 263 // String domain = entry.getKey(); 264 // List!(HttpCookie) lst = entry.getValue(); 265 // for (HttpCookie c : lst) { 266 // if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) || 267 // (c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) { 268 // if ((cookieJar.indexOf(c) != -1)) { 269 // // the cookie still in main cookie store 270 // if (!c.hasExpired()) { 271 // // don't add twice and make sure it's the proper 272 // // security level 273 // if ((secureLink || !c.getSecure()) && 274 // !cookies.contains(c)) { 275 // cookies.add(c); 276 // } 277 // } else { 278 // toRemove.add(c); 279 // } 280 // } else { 281 // // the cookie has beed removed from main store, 282 // // so also remove it from domain indexed store 283 // toRemove.add(c); 284 // } 285 // } 286 // } 287 // // Clear up the cookies that need to be removed 288 // for (HttpCookie c : toRemove) { 289 // lst.remove(c); 290 // cookieJar.remove(c); 291 292 // } 293 // toRemove.clear(); 294 // } 295 // } 296 297 // @param cookies [OUT] contains the found cookies 298 // @param cookieIndex the index 299 // @param key the prediction to decide whether or not 300 // a cookie in index should be returned 301 private void getInternal2(List!(HttpCookie) cookies, 302 Map!(string, List!(HttpCookie)) cookieIndex, 303 string key, bool secureLink) 304 { 305 foreach (string index; cookieIndex.byKey()) { 306 if (icmp(key, index) != 0) continue; 307 308 List!(HttpCookie) indexedCookies = cookieIndex.get(index); 309 // check the list of cookies associated with this domain 310 if (indexedCookies is null) 311 continue; 312 313 HttpCookie[] removedCookies; 314 foreach(HttpCookie ck; indexedCookies) { 315 if (cookieJar.indexOf(ck) != -1) { 316 // the cookie still in main cookie store 317 if (!ck.isExpired()) { 318 // don't add twice 319 if ((secureLink || !ck.getSecure()) && 320 !cookies.contains(ck)) 321 cookies.add(ck); 322 } else { 323 removedCookies ~= ck; 324 cookieJar.remove(ck); 325 } 326 } else { 327 // the cookie has beed removed from main store, 328 // so also remove it from domain indexed store 329 removedCookies ~= ck; 330 } 331 } 332 333 foreach(HttpCookie ck; removedCookies) { 334 indexedCookies.remove(ck); 335 } 336 } // end of cookieIndex iteration 337 } 338 339 // add 'cookie' indexed by 'index' into 'indexStore' 340 private void addIndex(Map!(string, List!HttpCookie) indexStore, 341 string index, 342 HttpCookie cookie) 343 { 344 if (!index.empty()) { 345 List!(HttpCookie) cookies = indexStore.get(index); 346 if (cookies !is null) { 347 // there may already have the same cookie, so remove it first 348 cookies.remove(cookie); 349 350 cookies.add(cookie); 351 } else { 352 cookies = new ArrayList!HttpCookie(); 353 cookies.add(cookie); 354 indexStore.put(index, cookies); 355 } 356 } 357 } 358 359 360 // 361 // for cookie purpose, the effective uri should only be http://host 362 // the path will be taken into account when path-match algorithm applied 363 // 364 private string getEffectiveURI(HttpURI uri) { 365 HttpURI effectiveURI = new HttpURI("http", 366 uri.getHost(), 367 uri.getPort(), 368 null // path component 369 ); 370 371 return effectiveURI.toString(); 372 } 373 374 // /** 375 // * Adds an array of {@link Cookie HTTP cookies}. Cookies are added individually and 376 // * in the given array order. If any of the given cookies has already expired it will 377 // * not be added, but existing values will still be removed. 378 // * 379 // * @param cookies the {@link Cookie cookies} to be added 380 // * 381 // * @see #addCookie(Cookie) 382 // * 383 // */ 384 // void addCookies(Cookie[] cookies) { 385 // if (cookies !is null) { 386 // foreach (Cookie cookie; cookies) { 387 // this.addCookie(cookie); 388 // } 389 // } 390 // } 391 392 // /** 393 // * Returns an immutable array of {@link Cookie cookies} that this HTTP 394 // * state currently contains. 395 // * 396 // * @return an array of {@link Cookie cookies}. 397 // */ 398 // Cookie[] getCookies() { 399 // lock.reader().lock(); 400 // scope(exit) lock.reader().unlock(); 401 402 // //create defensive copy so it won't be concurrently modified 403 // return cookies.toArray(); 404 // } 405 406 // /** 407 // * Removes all of {@link Cookie cookies} in this HTTP state 408 // * that have expired by the specified {@link java.util.Date date}. 409 // * 410 // * @return true if any cookies were purged. 411 // * 412 // * @see Cookie#isExpired(time) 413 // */ 414 // bool clearExpired(SysTime time) { 415 416 // lock.writer().lock(); 417 // scope(exit) lock.writer().unlock(); 418 419 // scope Cookie[] tempCookies; 420 // foreach (Cookie it; cookies) { 421 // if (it.isExpired(time)) { 422 // tempCookies ~= it; 423 // } 424 // } 425 426 // foreach(Cookie c; tempCookies) { 427 // cookies.remove(c); 428 // } 429 430 // return tempCookies.length > 0; 431 // } 432 433 // /** 434 // * Clears all cookies. 435 // */ 436 // void clear() { 437 // lock.writer().lock(); 438 // scope(exit) lock.writer().unlock(); 439 // cookies.clear(); 440 // } 441 442 // override string toString() { 443 // lock.reader().lock(); 444 // scope(exit) lock.reader().unlock(); 445 // return cookies.toString(); 446 // } 447 448 } 449 450 451 452 /** 453 * This cookie comparator can be used to compare identity of cookies. 454 * <p> 455 * Cookies are considered identical if their names are equal and 456 * their domain attributes match ignoring case. 457 * </p> 458 * 459 */ 460 class CookieIdentityComparator : Comparator!Cookie { 461 462 int compare(Cookie c1, Cookie c2) nothrow { 463 int res = cmp(c1.getName(), c2.getName()); // c1.getName().compareTo(c2.getName()); 464 if (res == 0) { 465 // do not differentiate empty and null domains 466 string d1 = c1.getDomain(); 467 if (d1 is null) { 468 d1 = ""; 469 } else if (d1.indexOf('.') == -1) { 470 d1 = d1 ~ ".local"; 471 } 472 string d2 = c2.getDomain(); 473 if (d2.empty()) { 474 d2 = ""; 475 } else if (d2.indexOf('.') == -1) { 476 d2 = d2 ~ ".local"; 477 } 478 res = icmp(d1, d2); 479 } 480 if (res == 0) { 481 string p1 = c1.getPath(); 482 if (p1.empty()) { 483 p1 = "/"; 484 } 485 string p2 = c2.getPath(); 486 if (p2.empty()) { 487 p2 = "/"; 488 } 489 res = cmp(p1, p2); 490 } 491 return res; 492 } 493 494 }