1 /** 2 Cookie based session support. 3 4 Copyright: © 2012-2013 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Jan Krüger, Sönke Ludwig, Ilya Shipunov 7 */ 8 module vibe.http.session; 9 10 import vibe.core.log; 11 import vibe.crypto.cryptorand; 12 13 import std.array; 14 import std.base64; 15 import std.traits : hasAliasing; 16 import std.variant; 17 18 //random number generator 19 //TODO: Use Whirlpool or SHA-512 here 20 private SHA1HashMixerRNG g_rng; 21 22 //The "URL and Filename safe" Base64 without padding 23 alias Base64URLNoPadding = Base64Impl!('-', '_', Base64.NoPadding); 24 25 26 /** 27 Represents a single HTTP session. 28 29 Indexing the session object with string keys allows to store arbitrary key/value pairs. 30 */ 31 struct Session { 32 private { 33 SessionStore m_store; 34 string m_id; 35 SessionStorageType m_storageType; 36 } 37 38 // created by the SessionStore using SessionStore.createSessionInstance 39 private this(SessionStore store, string id = null) 40 @safe { 41 assert(id.length > 0); 42 m_store = store; 43 m_id = id; 44 m_storageType = store.storageType; 45 } 46 47 /** Checks if the session is active. 48 49 This operator enables a $(D Session) value to be used in conditionals 50 to check if they are actially valid/active. 51 */ 52 bool opCast() const @safe { return m_store !is null; } 53 54 /// 55 unittest { 56 //import vibe.http.server; 57 // workaround for cyclic module ctor compiler error 58 class HTTPServerRequest { Session session; string[string] form; } 59 class HTTPServerResponse { Session startSession() { assert(false); } } 60 61 void login(scope HTTPServerRequest req, scope HTTPServerResponse res) 62 { 63 // TODO: validate username+password 64 65 // ensure that there is an active session 66 if (!req.session) req.session = res.startSession(); 67 68 // update session variables 69 req.session.set("loginUser", req.form["user"]); 70 } 71 } 72 73 /// Returns the unique session id of this session. 74 @property string id() const @safe { return m_id; } 75 76 /// Queries the session for the existence of a particular key. 77 bool isKeySet(string key) @safe { return m_store.isKeySet(m_id, key); } 78 79 /** Gets a typed field from the session. 80 */ 81 const(T) get(T)(string key, lazy T def_value = T.init) 82 @trusted { // Variant, deserializeJson/deserializeBson 83 static assert(!hasAliasing!T, "Type "~T.stringof~" contains references, which is not supported for session storage."); 84 auto val = m_store.get(m_id, key, serialize(def_value)); 85 return deserialize!T(val); 86 } 87 88 /** Sets a typed field to the session. 89 */ 90 void set(T)(string key, T value) 91 { 92 static assert(!hasAliasing!T, "Type "~T.stringof~" contains references, which is not supported for session storage."); 93 m_store.set(m_id, key, serialize(value)); 94 } 95 96 // Removes a field from a session 97 void remove(string key) @safe { m_store.remove(m_id, key); } 98 99 /** 100 Enables foreach-iteration over all keys of the session. 101 */ 102 int opApply(scope int delegate(string key) @safe del) 103 @safe { 104 return m_store.iterateSession(m_id, del); 105 } 106 /// 107 unittest { 108 //import vibe.http.server; 109 // workaround for cyclic module ctor compiler error 110 class HTTPServerRequest { Session session; } 111 class HTTPServerResponse { import vibe.core.stream; OutputStream bodyWriter() @safe { assert(false); } string contentType; } 112 113 // sends all session entries to the requesting browser 114 // assumes that all entries are strings 115 void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res) 116 { 117 res.contentType = "text/plain"; 118 req.session.opApply((key) @safe { 119 res.bodyWriter.write(key ~ ": " ~ req.session.get!string(key) ~ "\n"); 120 return 0; 121 }); 122 } 123 } 124 125 package void destroy() @safe { m_store.destroy(m_id); } 126 127 private Variant serialize(T)(T val) 128 { 129 import vibe.data.json; 130 import vibe.data.bson; 131 132 final switch (m_storageType) with (SessionStorageType) { 133 case native: return () @trusted { return Variant(val); } (); 134 case json: return () @trusted { return Variant(serializeToJson(val)); } (); 135 case bson: return () @trusted { return Variant(serializeToBson(val)); } (); 136 } 137 } 138 139 private T deserialize(T)(ref Variant val) 140 { 141 import vibe.data.json; 142 import vibe.data.bson; 143 144 final switch (m_storageType) with (SessionStorageType) { 145 case native: return () @trusted { return val.get!T; } (); 146 case json: return () @trusted { return deserializeJson!T(val.get!Json); } (); 147 case bson: return () @trusted { return deserializeBson!T(val.get!Bson); } (); 148 } 149 } 150 } 151 152 153 /** 154 Interface for a basic session store. 155 156 A session store is responsible for storing the id and the associated key/value pairs of a 157 session. 158 */ 159 interface SessionStore { 160 @safe: 161 162 /// Returns the internal type used for storing session keys. 163 @property SessionStorageType storageType() const; 164 165 /// Creates a new session. 166 Session create(); 167 168 /// Opens an existing session. 169 Session open(string id); 170 171 /// Sets a name/value pair for a given session. 172 void set(string id, string name, Variant value); 173 174 /// Returns the value for a given session key. 175 Variant get(string id, string name, lazy Variant defaultVal); 176 177 /// Determines if a certain session key is set. 178 bool isKeySet(string id, string key); 179 180 /// Removes a key from a session 181 void remove(string id, string key); 182 183 /// Terminates the given session. 184 void destroy(string id); 185 186 /// Iterates all keys stored in the given session. 187 int iterateSession(string id, scope int delegate(string key) @safe del); 188 189 /// Creates a new Session object which sources its contents from this store. 190 protected final Session createSessionInstance(string id = null) 191 { 192 if (!id.length) { 193 ubyte[64] rand; 194 if (!g_rng) g_rng = new SHA1HashMixerRNG(); 195 g_rng.read(rand); 196 id = () @trusted { return cast(immutable)Base64URLNoPadding.encode(rand); } (); 197 } 198 return Session(this, id); 199 } 200 } 201 202 enum SessionStorageType { 203 native, 204 json, 205 bson 206 } 207 208 209 /** 210 Session store for storing a session in local memory. 211 212 If the server is running as a single instance (no thread or process clustering), this kind of 213 session store provies the fastest and simplest way to store sessions. In any other case, 214 a persistent session store based on a database is necessary. 215 */ 216 final class MemorySessionStore : SessionStore { 217 @safe: 218 219 private { 220 Variant[string][string] m_sessions; 221 } 222 223 @property SessionStorageType storageType() 224 const { 225 return SessionStorageType.native; 226 } 227 228 Session create() 229 { 230 auto s = createSessionInstance(); 231 m_sessions[s.id] = null; 232 return s; 233 } 234 235 Session open(string id) 236 { 237 auto pv = id in m_sessions; 238 return pv ? createSessionInstance(id) : Session.init; 239 } 240 241 void set(string id, string name, Variant value) 242 @trusted { // Variant 243 m_sessions[id][name] = value; 244 foreach(k, v; m_sessions[id]) logTrace("Csession[%s][%s] = %s", id, k, v); 245 } 246 247 Variant get(string id, string name, lazy Variant defaultVal) 248 @trusted { // Variant 249 assert(id in m_sessions, "session not in store"); 250 foreach(k, v; m_sessions[id]) logTrace("Dsession[%s][%s] = %s", id, k, v); 251 if (auto pv = name in m_sessions[id]) { 252 return *pv; 253 } else { 254 return defaultVal; 255 } 256 } 257 258 bool isKeySet(string id, string key) 259 { 260 return (key in m_sessions[id]) !is null; 261 } 262 263 void remove(string id, string key) 264 { 265 m_sessions[id].remove(key); 266 } 267 268 void destroy(string id) 269 { 270 m_sessions.remove(id); 271 } 272 273 int delegate(int delegate(ref string key, ref Variant value) @safe) @safe iterateSession(string id) 274 { 275 assert(id in m_sessions, "session not in store"); 276 int iterator(int delegate(ref string key, ref Variant value) @safe del) 277 @safe { 278 foreach( key, ref value; m_sessions[id] ) 279 if( auto ret = del(key, value) != 0 ) 280 return ret; 281 return 0; 282 } 283 return &iterator; 284 } 285 286 int iterateSession(string id, scope int delegate(string key) @safe del) 287 @trusted { // hash map iteration 288 assert(id in m_sessions, "session not in store"); 289 foreach (key; m_sessions[id].byKey) 290 if (auto ret = del(key)) 291 return ret; 292 return 0; 293 } 294 }