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 }