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 }