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