1 /**
2 * Copyright © DiamondMVC 2018
3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module diamond.http.sessions;
7 
8 import diamond.core.apptype;
9 
10 static if (isWeb)
11 {
12   import core.time : msecs, minutes;
13   import std.datetime : Clock, SysTime;
14   import std.variant : Variant;
15 
16   import vibe.d : runTask;
17 
18   import diamond.core.webconfig;
19   import diamond.core.collections;
20   import diamond.errors.checks;
21   import diamond.http.cookies;
22   import diamond.http.client;
23 
24   /// The name of the session cookie.
25   private static const __gshared sessionCookieName = "__D_SESSION";
26 
27   /// The collection of currently stored sessions.
28   private __gshared InternalHttpSession[string] _sessions;
29 
30   /// The next session group id.
31   private size_t nextSessionGroupId;
32 
33   /// The sessions met.
34   private size_t sessionsMet;
35 
36   /// Wrapper for the internal http session.
37   private final class InternalHttpSession
38   {
39     /// The id of the session.
40     string id;
41 
42     /// The ip address of the session.
43     string ipAddress;
44 
45     /// The time when the session ends.
46     SysTime endTime;
47 
48     /// The value os the session.
49     Variant[string] values;
50 
51     /// Cached views.
52     HashSet!string cachedViews;
53 
54     /// The directory for the session view cache.
55     string directory;
56 
57     final:
58     /// Creates a new http session instance.
59     this()
60     {
61       cachedViews = new HashSet!string;
62     }
63   }
64 
65   /// Wrapper around a http session.
66   final class HttpSession
67   {
68     private:
69     /// The client.
70     HttpClient _client;
71 
72     /// The session.
73     InternalHttpSession _session;
74 
75     /**
76     * Creates a new http session.
77     * Params:
78     *   client =  The client.
79     *   session = The internal http session.
80     */
81     this(HttpClient client, InternalHttpSession session)
82     {
83       _client = enforceInput(client, "Cannot create a session without a client.");
84       _session = enforceInput(session, "Cannot create a session without an internal session.");
85     }
86 
87     public:
88     final:
89     @property
90     {
91       /// Gets the session id.
92       string id() { return _session.id; }
93     }
94     
95     /**
96     * Gets a session value.
97     * Params:
98     *   name =         The name of the value to retrieve.
99     *   defaultValue = The default value to return if no value could be retrieved.
100     * Returns:
101     *   Returns the value retrieved or defaultValue if not found.
102     */
103     T getValue(T = string)(string name, lazy T defaultValue = T.init)
104     {
105       Variant value = _session.values.get(name, Variant.init);
106 
107       if (!value.hasValue)
108       {
109         return defaultValue;
110       }
111 
112       return value.get!T;
113     }
114 
115     /**
116     * Sets a session value.
117     * Params:
118     *   name =      The name of the value.
119     *   value =     The value.
120     */
121     void setValue(T = string)(string name, T value)
122     {
123       _session.values[name] = value;
124     }
125 
126     /**
127     * Removes a session value.
128     * Params:
129     *   name = The name of the value to remove.
130     */
131     void removeValue(string name)
132     {
133       if (_session.values && name in _session.values)
134       {
135         _session.values.remove(name);
136       }
137     }
138 
139     /**
140     * Caches a view in the session.
141     * Params:
142     *   viewName = The view to cache.
143     *   result =   The result of the view to cache.
144     */
145     void cacheView(string viewName, string result)
146     {
147       if (!webConfig.shouldCacheViews)
148       {
149         return;
150       }
151 
152       _session.cachedViews.add(viewName);
153 
154       import std.file : exists, write, mkdirRecurse;
155 
156       if (!exists(_session.directory))
157       {
158         mkdirRecurse(_session.directory);
159       }
160 
161       write(_session.directory ~ "/" ~ viewName ~ ".html", result);
162     }
163 
164     /**
165     * Gets a view from the session cache.
166     * Params:
167     *   viewName = The view to retrieve.
168     * Returns:
169     *   The result of the cached view if found, null otherwise.
170     */
171     string getCachedView(string viewName)
172     {
173       if (!webConfig.shouldCacheViews)
174       {
175         return null;
176       }
177 
178       import std.file : exists, readText;
179 
180       if (_session.cachedViews[viewName])
181       {
182         auto sessionViewFile = _session.directory ~ "/" ~ viewName ~ ".html";
183 
184         if (exists(sessionViewFile))
185         {
186           return readText(sessionViewFile);
187         }
188       }
189 
190       return null;
191     }
192 
193     /**
194     * Updates the session end time.
195     * Params:
196     *   newEndTime = The new end time.
197     */
198     void updateEndTime(SysTime newEndTime)
199     {
200       _session.endTime = newEndTime;
201     }
202 
203     /// Clears the session values for a session.
204     void clearValues()
205     {
206       _session.values.clear();
207     }
208 
209     /**
210     * Checks whether a value is present in the session.
211     * Params:
212     *   name = The name of the value to check for presence.
213     * Returns:
214     *   True if the value is present, false otherwise.
215     */
216     bool hasValue(string name)
217     {
218       return _session.values.get(name, Variant.init).hasValue;
219     }
220   }
221 
222   /**
223   * Gets a session.
224   * Params:
225   *   client =                 The client
226   *   createSessionIfInvalid = Boolean determining whether a new session should be created if the session is invalid.
227   * Returns:
228   *   Returns the session.
229   */
230   package(diamond.http) HttpSession getSession
231   (
232     HttpClient client,
233     bool createSessionIfInvalid = true
234   )
235   {
236     // Checks whether the request has already got its session assigned.
237     auto cachedSession = client.getContext!HttpSession(sessionCookieName);
238 
239     if (cachedSession)
240     {
241       return cachedSession;
242     }
243 
244     auto sessionId = client.cookies.get(sessionCookieName);
245     auto session = _sessions.get(sessionId, null);
246 
247     if (createSessionIfInvalid &&
248       (
249         !session ||
250         session.ipAddress != client.ipAddress ||
251         Clock.currTime() >= session.endTime
252       )
253     )
254     {
255       client.cookies.remove(sessionCookieName);
256 
257       return createSession(client);
258     }
259 
260     if (!session)
261     {
262       return null;
263     }
264 
265     auto httpSession = new HttpSession(client, session);
266     client.addContext(sessionCookieName, httpSession);
267 
268     return httpSession;
269   }
270 
271   /**
272   * Creates a http session.
273   * Params:
274   *   client =  The client.
275   * Returns:
276   *   Returns the session.
277   */
278   HttpSession createSession(HttpClient client)
279   {
280     auto clientSession = getSession(client, false);
281 
282     if (clientSession)
283     {
284       return clientSession;
285     }
286 
287     auto session = new InternalHttpSession;
288 
289     import diamond.security.tokens.sessiontoken;
290 
291     session.ipAddress = client.ipAddress;
292     session.id = sessionToken.generate(session.ipAddress);
293     _sessions[session.id] = session;
294 
295     if (webConfig.shouldCacheViews)
296     {
297       import std.conv : to;
298 
299       sessionsMet++;
300 
301       if (sessionsMet >= 1000)
302       {
303         sessionsMet = 0;
304         nextSessionGroupId++;
305       }
306 
307       session.directory = "sessions/" ~ to!string(nextSessionGroupId) ~ "/" ~ session.id[$-52 .. $] ~ "/";
308     }
309 
310     client.cookies.create(sessionCookieName, session.id, webConfig.sessionAliveTime * 60);
311     session.endTime = Clock.currTime();
312     session.endTime = session.endTime + webConfig.sessionAliveTime.minutes;
313 
314     clientSession = new HttpSession(client, session);
315     client.addContext(sessionCookieName, clientSession);
316 
317     runTask((InternalHttpSession session) { invalidateSession(session, 3); }, session);
318 
319     return clientSession;
320   }
321 
322   /**
323   * Invalidates a session.
324   * Params:
325   *   session = The session to invalidate.
326   *   retries = The amount of retries left, if it failed to remove the session.
327   *   isRetry = Boolean determining whether the invalidation is a retry or not.
328   */
329   private void invalidateSession(InternalHttpSession session, size_t retries, bool isRetry = false)
330   {
331     import vibe.core.core : sleep;
332 
333     auto time = isRetry ? 100.msecs : (webConfig.sessionAliveTime + 2).minutes;
334 
335     sleep(time);
336 
337     try
338     {
339       // The endtime differs from the default, so we cycle once more.
340       if (Clock.currTime() < session.endTime)
341       {
342         runTask((InternalHttpSession session) { invalidateSession(session, 3); }, session);
343       }
344       else
345       {
346         if (webConfig.shouldCacheViews)
347         {
348           try
349           {
350             import std.file : exists, rmdirRecurse;
351 
352             if (exists(session.directory))
353             {
354               rmdirRecurse(session.directory);
355             }
356           }
357           catch (Throwable t) { }
358         }
359 
360         _sessions.remove(session.id);
361       }
362     }
363     catch (Throwable)
364     {
365       if (retries)
366       {
367         runTask((InternalHttpSession s, size_t r) { invalidateSession(s, r, true); }, session, retries - 1);
368       }
369     }
370   }
371 }