1 module hunt.http.server.HttpSession;
2 
3 import hunt.http.Exceptions;
4 
5 import hunt.collection.HashMap;
6 import hunt.collection.Map;
7 import hunt.Exceptions;
8 import hunt.util.CompilerHelper;
9 import hunt.util.DateTime;
10 import hunt.util.Lifecycle;
11 
12 import std.algorithm;
13 import std.conv;
14 import std.digest.sha;
15 import std.json;
16 import std.range;
17 import std.string;
18 import std.traits;
19 import std.variant;
20 
21 
22 enum string DefaultSessionIdName = "hunt_session";
23 
24 
25 /**
26  * 
27  */
28 class HttpSession {
29 
30     private string id;
31     private long creationTime;
32     private long lastAccessedTime;
33     private int maxInactiveInterval;
34     private JSONValue attributes;
35     private bool newSession;
36 
37     string getId() {
38         return id;
39     }
40 
41     void setId(string id) {
42         this.id = id;
43     }
44 
45     long getCreationTime() {
46         return creationTime;
47     }
48 
49     void setCreationTime(long creationTime) {
50         this.creationTime = creationTime;
51     }
52 
53     long getLastAccessedTime() {
54         return lastAccessedTime;
55     }
56 
57     void setLastAccessedTime(long lastAccessedTime) {
58         this.lastAccessedTime = lastAccessedTime;
59     }
60 
61     /**
62      * Get the max inactive interval. The time unit is second.
63      *
64      * @return The max inactive interval.
65      */
66     int getMaxInactiveInterval() {
67         return maxInactiveInterval;
68     }
69 
70     /**
71      * Set the max inactive interval. The time unit is second.
72      *
73      * @param maxInactiveInterval The max inactive interval.
74      */
75     void setMaxInactiveInterval(int maxInactiveInterval) {
76         this.maxInactiveInterval = maxInactiveInterval;
77     }
78 
79     ref JSONValue getAttributes() {
80         return attributes;
81     }
82 
83     void setAttributes(ref JSONValue attributes) {
84         this.attributes = attributes;
85     }
86 
87     void setAttribute(T)(string name, T value) {
88         this.attributes[name] = value;
89     }
90 
91     T getAttribute(T=string)(string name) {
92         JSONValue jv = attributes[name];
93         static if(CompilerHelper.isGreaterThan(2092)) {
94             return jv.get!T();
95         } else {
96             static if (is(Unqual!T == string)) {
97                 return jv.str;
98             } else static if (is(Unqual!T == bool)) {
99                 return jv.boolean;
100             } else static if (isFloatingPoint!T) {
101                 switch (type) {
102                 case JSONType.float_:
103                     return cast(T) jv.floating;
104                 case JSONType.uinteger:
105                     return cast(T) jv.uinteger;
106                 case JSONType.integer:
107                     return cast(T) jv.integer;
108                 default:
109                     throw new JSONException("JSONValue is not a number type");
110                 }
111             } else static if (__traits(isUnsigned, T)) {
112                 return cast(T) jv.uinteger;
113             } else static if (isSigned!T) {
114                 return cast(T) jv.integer;
115             } else {
116                 static assert(false, "Unsupported type: " ~ T.stringof);
117             }                    
118         }
119     }
120 
121     bool isNewSession() {
122         return newSession;
123     }
124 
125     void setNewSession(bool newSession) {
126         this.newSession = newSession;
127     }
128 
129     bool isValid() {
130         long currentTime = DateTime.currentTimeMillis();
131         return (currentTime - lastAccessedTime) < (maxInactiveInterval * 1000);
132     }
133 
134     void set(T)(string name, T value) {
135         this.attributes[name] = JSONValue(value);
136     }
137 
138     T get(T = string)(string name, T defaultValue = T.init) {
139         if(attributes.isNull)
140             return defaultValue;
141 
142         const(JSONValue)* itemPtr = name in attributes;
143         if (itemPtr is null)
144             return defaultValue;
145         static if (!is(T == string) && isDynamicArray!T && is(T : U[], U)) {
146             U[] r;
147             foreach (JSONValue jv; itemPtr.array) {
148                 r ~= get!U(jv);
149             }
150             return r;
151         }
152         else {
153             return get!T(*itemPtr);
154         }
155     }
156 
157     private static T get(T = string)(JSONValue itemPtr) {
158         static if (is(T == string)) {
159             return itemPtr.str;
160         }
161         else static if (isIntegral!T) {
162             return cast(T) itemPtr.integer;
163         }
164         else static if (isFloatingPoint!T) {
165             return cast(T) itemPtr.floating;
166         }
167         else {
168             static assert(false, "Unsupported type: " ~ typeid(T).name);
169         }
170     }
171 
172     void remove(string key) {
173         if(attributes.isNull)
174             return;
175 
176         JSONValue json;
177         foreach (string _key, ref value; attributes) {
178             if (_key != key) {
179                 json[_key] = value;
180             }
181         }
182 
183         attributes = json;
184     }
185 
186     void remove(string[] keys) {
187         if(attributes.isNull)
188             return;
189 
190         JSONValue json;
191         foreach (string _key, ref value; attributes) {
192             if (!keys.canFind(_key)) {
193                 json[_key] = value;
194             }
195         }
196 
197         attributes = json;
198     }    
199 
200     alias forget = remove;
201 
202     string[] keys() {
203         if(attributes.isNull)
204             return null;
205             
206         string[] ret;
207 
208         foreach (string key, ref value; attributes) {
209             ret ~= key;
210         }
211 
212         return ret;
213     }
214 
215     /**
216      * Get all of the session data.
217      *
218      * @return array
219      */
220     string[string] all() {
221         if (attributes.isNull)
222             return null;
223 
224         string[string] v;
225         foreach (string key, ref JSONValue value; attributes) {
226             if (value.type == JSONType..string)
227                 v[key] = value.str;
228             else
229                 v[key] = value.toString();
230         }
231 
232         return v;
233     }
234 
235     /**
236      * Checks if a key exists.
237      *
238      * @param  string|array  key
239      * @return bool
240      */
241     bool exists(string key) {
242         if (attributes.isNull)
243             return false;
244         const(JSONValue)* item = key in attributes;
245         return item !is null;
246     }
247 
248     /**
249      * Checks if a key is present and not null.
250      *
251      * @param  string|array  key
252      * @return bool
253      */
254     bool has(string key) {
255         if (attributes.isNull)
256             return false;
257 
258         auto item = key in attributes;
259         if ((item !is null) && (!item.str.empty))
260             return true;
261         else
262             return false;
263     }
264 
265     /**
266      * Get the value of a given key and then forget it.
267      *
268      * @param  string  key
269      * @param  string  default
270      * @return mixed
271      */
272     void pull(string key, string value) {
273         attributes[key] = value;
274     }
275 
276     /**
277      * Determine if the session contains old input.
278      *
279      * @param  string  key
280      * @return bool
281      */
282     bool hasOldInput(string key) {
283         string old = getOldInput(key);
284         return !old.empty;
285     }
286 
287     /**
288      * Get the requested item from the flashed input array.
289      *
290      * @param  string  key
291      * @param  mixed   default
292      * @return mixed
293      */
294     string[string] getOldInput(string[string] defaults = null) {
295         string v = get("_old_input");
296         if (v.empty)
297             return defaults;
298         else
299             return to!(string[string])(v);
300     }
301 
302     /// ditto
303     string getOldInput(string key, string defaults = null) {
304         string old = get("_old_input");
305         string[string] v = to!(string[string])(old);
306         return v.get(key, defaults);
307     }
308 
309     /**
310      * Replace the given session attributes entirely.
311      *
312      * @param  array  attributes
313      * @return void
314      */
315     void replace(string[string] attributes) {
316         this.attributes = JSONValue.init;
317         put(attributes);
318     }
319 
320     /**
321      * Put a key / value pair or array of key / value pairs in the session.
322      *
323      * @param  string|array  key
324      * @param  mixed       value
325      * @return void
326      */
327     void put(T = string)(string key, T value) {
328         attributes[key] = value;
329     }
330 
331     /// ditto
332     void put(string[string] pairs) {
333         foreach (string key, string value; pairs)
334             attributes[key] = value;
335     }
336 
337     /**
338      * Get an item from the session, or store the default value.
339      *
340      * @param  string  key
341      * @param  \Closure  callback
342      * @return mixed
343      */
344     string remember(string key, string value) {
345         string v = this.get(key);
346         if (!v.empty)
347             return v;
348 
349         this.put(key, value);
350         return value;
351     }
352 
353     /**
354      * Push a value onto a session array.
355      *
356      * @param  string  key
357      * @param  mixed   value
358      * @return void
359      */
360     void push(T = string)(string key, T value) {
361         T[] array = this.get!(T[])(key);
362         array ~= value;
363 
364         this.put(key, array);
365     }
366 
367     /**
368      * Flash a key / value pair to the session.
369      *
370      * @param  string  key
371      * @param  mixed   value
372      * @return void
373      */
374     void flash(T = string)(string key, T value) {
375         this.put(key, value);
376         this.push("_flash.new", key);
377         this.removeFromOldFlashData([key]);
378     }
379 
380     /**
381      * Flash a key / value pair to the session for immediate use.
382      *
383      * @param  string key
384      * @param  mixed value
385      * @return void
386      */
387     void now(T = string)(string key, T value) {
388         this.put(key, value);
389         this.push("_flash.old", key);
390     }
391 
392     /**
393      * Reflash all of the session flash data.
394      *
395      * @return void
396      */
397     public void reflash() {
398         this.mergeNewFlashes(this.get!(string[])("_flash.old"));
399         this.put!(string[])("_flash.old", []);
400     }
401 
402     /**
403      * Reflash a subset of the current flash data.
404      *
405      * @param  array|mixed  keys
406      * @return void
407      */
408     void keep(string[] keys...) {
409         string[] ks = keys.dup;
410         mergeNewFlashes(ks);
411         removeFromOldFlashData(ks);
412     }
413 
414     /**
415      * Merge new flash keys into the new flash array.
416      *
417      * @param  array  keys
418      * @return void
419      */
420     protected void mergeNewFlashes(string[] keys) {
421         string[] oldKeys = this.get!(string[])("_flash.new");
422         string[] values = oldKeys ~ keys;
423         values = values.sort().uniq().array;
424 
425         this.put("_flash.new", values);
426     }
427 
428     /**
429      * Remove the given keys from the old flash data.
430      *
431      * @param  array  keys
432      * @return void
433      */
434     protected void removeFromOldFlashData(string[] keys) {
435         string[] olds = this.get!(string[])("_flash.old");
436         string[] news = olds.remove!(x => keys.canFind(x));
437         this.put("_flash.old", news);
438     }
439 
440     /**
441      * Flash an input array to the session.
442      *
443      * @param  array  value
444      * @return void
445      */
446     void flashInput(string[string] value) {
447         flash("_old_input", to!string(value));
448     }
449 
450     /**
451      * Flush the session data and regenerate the ID.
452      *
453      * @return bool
454      */
455     // bool invalidate()
456     // {
457     // 	flush();
458 
459     // 	return migrate(true);
460     // }
461 
462     /**
463      * Save the session data to storage.
464      */
465     void save() {
466         string[] olds = this.get!(string[])("_flash.old");
467 
468         remove(olds);
469 
470         // attributes
471         string[] news = this.get!(string[])("_flash.new");
472         this.put("_flash.old", news);
473         this.put!(string[])("_flash.new", []);
474 
475         news = this.get!(string[])("_flash.new");
476     }
477 
478     override bool opEquals(Object o) {
479         if (this is o)
480             return true;
481         HttpSession that = cast(HttpSession) o;
482         if (that is null)
483             return false;
484         return id == that.id;
485     }
486 
487     override size_t toHash() @trusted nothrow {
488         return hashOf(id);
489     }
490 
491     static HttpSession create(string id, int maxInactiveInterval) {
492         long currentTime = DateTime.currentTimeMillis();
493         HttpSession session = new HttpSession();
494         session.setId(id);
495         session.setMaxInactiveInterval(maxInactiveInterval);
496         session.setCreationTime(currentTime);
497         session.setLastAccessedTime(session.getCreationTime());
498         // session.setAttributes(new HashMap!(string, Object)());
499         session.setNewSession(true);
500         return session;
501     }
502 
503     static string toJson(HttpSession session) {
504         JSONValue j;
505         j["CreationTime"] = session.creationTime;
506         j["attr"] = session.attributes;
507         return j.toString();
508     }
509 
510     static HttpSession fromJson(string id, string json) {
511         JSONValue j = parseJSON(json);
512         long currentTime = DateTime.currentTimeMillis();
513         HttpSession session = new HttpSession();
514         session.setId(id);
515         session.setCreationTime(j["CreationTime"].integer);
516         session.setLastAccessedTime(currentTime);
517         session.setNewSession(false);
518         session.attributes = j["attr"];
519 
520         return session;
521     }
522 
523     static string generateSessionId(string sessionName = DefaultSessionIdName) {
524         SHA1 hash;
525         hash.start();
526         hash.put(getRandom);
527         ubyte[20] result = hash.finish();
528         string str = toLower(toHexString(result));
529 
530         return str;
531     }
532 }
533 
534 ubyte[] getRandom(ushort len = 64)
535 {
536     assert(len);
537     ubyte[] buffer;
538     buffer.length = len;
539     version(Windows){
540         import core.sys.windows.wincrypt;
541         import core.sys.windows.windef;
542         HCRYPTPROV hCryptProv;
543         assert(CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT) != 0);
544         CryptGenRandom(hCryptProv, cast(DWORD)buffer.length, buffer.ptr);
545         scope(exit)CryptReleaseContext(hCryptProv, 0);
546     }else version(SecureARC4Random){
547         arc4random_buf(buffer.ptr, len);
548     }else{
549         import core.stdc.stdio : FILE, _IONBF, fopen, fclose, fread, setvbuf;
550         auto file = fopen("/dev/urandom","rb");
551         scope(exit)fclose(file);
552         if(file is null)throw new Exception("Failed to open /dev/urandom"); 
553         if(setvbuf(file, null, 0, _IONBF) != 0)throw new 
554             Exception("Failed to disable buffering for random number file handle");
555         if(fread(buffer.ptr, buffer.length, 1, file) != 1)throw new
556             Exception("Failed to read next random number");
557     }
558     return buffer;
559 }
560 
561 /**
562  * 
563  */
564 interface SessionStore : Lifecycle {
565 
566     bool contains(string key);
567 
568     bool remove(string key);
569 
570     bool put(string key, HttpSession value);
571 
572     HttpSession get(string key);
573 
574     int size();
575 
576 }
577 
578 /**
579  * 
580  */
581 interface HttpSessionHandler {
582 
583     HttpSession getSessionById(string id);
584 
585     HttpSession getSession();
586 
587     HttpSession getSession(bool create);
588 
589     HttpSession getAndCreateSession(int maxAge);
590 
591     int getSessionSize();
592 
593     bool removeSession();
594 
595     bool removeSessionById(string id);
596 
597     bool updateSession(HttpSession httpSession);
598 
599     bool isRequestedSessionIdFromURL();
600 
601     bool isRequestedSessionIdFromCookie();
602 
603     string getRequestedSessionId();
604 
605     string getSessionIdParameterName();
606 }