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 }