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 }