1 /**
2 	MongoDB based HTTP session store.
3 
4 	Copyright: © 2017 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vibe.db.mongo.sessionstore;
9 
10 import vibe.data.json;
11 import vibe.db.mongo.mongo;
12 import vibe.http.session;
13 import core.time;
14 import std.datetime : Clock, SysTime, UTC;
15 import std.typecons : Nullable;
16 import std.variant;
17 
18 ///
19 unittest {
20 	import vibe.core.core : runApplication;
21 	import vibe.db.mongo.sessionstore : MongoSessionStore;
22 	import vibe.http.server : HTTPServerSettings, listenHTTP;
23 	import vibe.http.router : URLRouter;
24 	import core.time : hours;
25 
26 	void main()
27 	{
28 		auto store = new MongoSessionStore("mongodb://127.0.0.1/mydb", "sessions");
29 		store.expirationTime = 5.hours;
30 
31 		auto settings = new HTTPServerSettings("127.0.0.1:8080");
32 		settings.sessionStore = store;
33 
34 		auto router = new URLRouter;
35 		// TODO: add some routes
36 
37 		listenHTTP(settings, router);
38 
39 		runApplication();
40 	}
41 }
42 
43 
44 final class MongoSessionStore : SessionStore {
45 @safe:
46 	private {
47 		MongoCollection m_sessions;
48 		Duration m_expirationTime = Duration.max;
49 	}
50 
51 	/** Constructs a new MongoDB session store.
52 
53 		Params:
54 			url = URL of the MongoDB database (e.g. `"mongodb://localhost/mydb"`)
55 			collection = Optional collection name to store the sessions in
56 	*/
57 	this(string url, string collection = "sessions")
58 	{
59 		import std.exception : enforce;
60 
61 		MongoClientSettings settings;
62 		enforce(parseMongoDBUrl(settings, url),
63 			"Failed to parse MongoDB URL.");
64 		auto db = connectMongoDB(settings).getDatabase(settings.database);
65 		m_sessions = db[collection];
66 	}
67 
68 	/** Constructs a new MongoDB session store using an existing DB.
69 
70 		Params:
71 			db = the connected MongoDB database.
72 			collection = the collection name for the sessions collection.
73 	*/
74 	this(MongoDatabase db, string collection = "sessions")
75 	{
76 		m_sessions = db[collection];
77 	}
78 
79 	/** Constructs a new MongoDB session store using a collection object.
80 
81 		Params:
82 			collection = the collection to store sessions in.
83 	*/
84 	this(MongoCollection collection)
85 	{
86 		m_sessions = collection;
87 	}
88 
89 	/** The duration without access after which a session expires.
90 	*/
91 	@property Duration expirationTime() const { return m_expirationTime; }
92 	/// ditto
93 	@property void expirationTime(Duration dur)
94 	{
95 		import std.typecons : tuple;
96 		IndexModel[1] index;
97 		index[0].add("time", 1);
98 		index[0].options.expireAfter = dur;
99 		m_sessions.createIndexes(index[]);
100 		m_expirationTime = dur;
101 	}
102 
103 	@property SessionStorageType storageType() const { return SessionStorageType.bson; }
104 
105 	Session create()
106 	{
107 		auto s = createSessionInstance();
108 		m_sessions.insertOne(SessionEntry(s.id, Clock.currTime(UTC())));
109 		return s;
110 	}
111 
112 	Session open(string id)
113 	{
114 		auto res = m_sessions.findAndModify(["_id": id], ["$set": ["time": Clock.currTime(UTC())]], ["_id": 1]);
115 		if (!res.isNull) return createSessionInstance(id);
116 		return Session.init;
117 	}
118 
119 	void set(string id, string name, Variant value)
120 	@trusted {
121 		m_sessions.updateOne(["_id": id], ["$set": [name.escape: value.get!Bson, "time": Clock.currTime(UTC()).serializeToBson]]);
122 	}
123 
124 	Variant get(string id, string name, lazy Variant defaultVal)
125 	@trusted {
126 		auto f = name.escape;
127 		FindOptions options;
128 		options.projection = Bson([f: Bson(1)]);
129 		auto r = m_sessions.findOne(["_id": id], options);
130 		if (r.isNull) return defaultVal;
131 		auto v = r.tryIndex(f);
132 		if (v.isNull) return defaultVal;
133 		return Variant(v.get);
134 	}
135 
136 	bool isKeySet(string id, string key)
137 	{
138 		auto f = key.escape;
139 		FindOptions options;
140 		options.projection = Bson([f: Bson(1)]);
141 		auto r = m_sessions.findOne(["_id": id], options);
142 		if (r.isNull) return false;
143 		return !r.tryIndex(f).isNull;
144 	}
145 
146 	void remove(string id, string key)
147 	{
148 		m_sessions.updateOne(["_id": id], ["$unset": [key.escape: 1]]);
149 	}
150 
151 	void destroy(string id)
152 	{
153 		m_sessions.deleteOne(["_id": id]);
154 	}
155 
156 	int iterateSession(string id, scope int delegate(string key) @safe del)
157 	{
158 		import std.algorithm.searching : startsWith;
159 
160 		auto r = m_sessions.findOne(["_id": id]);
161 		foreach (k, _; r.byKeyValue) {
162 			if (k.startsWith("f_")) {
163 				auto f = k.unescape;
164 				if (auto ret = del(f))
165 					return ret;
166 			}
167 		}
168 		return 0;
169 	}
170 
171 	private static struct SessionEntry {
172 		string _id;
173 		SysTime time;
174 	}
175 }
176 
177 
178 private string escape(string field_name)
179 @safe {
180 	import std.array : appender;
181 	import std.format : formattedWrite;
182 
183 	auto ret = appender!string;
184 	ret.reserve(field_name.length + 2);
185 	ret.put("f_");
186 	foreach (char ch; field_name) {
187 		switch (ch) {
188 			default:
189 				ret.formattedWrite("+%02X", cast(int)ch);
190 				break;
191 			case 'a': .. case 'z':
192 			case 'A': .. case 'Z':
193 			case '0': .. case '9':
194 			case '_', '-':
195 				ret.put(ch);
196 				break;
197 		}
198 	}
199 	return ret.data;
200 }
201 
202 private string unescape(string key)
203 @safe {
204 	import std.algorithm.searching : startsWith;
205 	import std.array : appender;
206 	import std.conv : to;
207 
208 	assert(key.startsWith("f_"));
209 	key = key[2 .. $];
210 	auto ret = appender!string;
211 	ret.reserve(key.length);
212 	while (key.length) {
213 		if (key[0] == '+') {
214 			ret.put(cast(char)key[1 .. 3].to!int(16));
215 			key = key[3 .. $];
216 		} else {
217 			ret.put(key[0]);
218 			key = key[1 .. $];
219 		}
220 	}
221 	return ret.data;
222 }
223 
224 @safe unittest {
225 	void test(string raw, string enc) {
226 		assert(escape(raw) == enc);
227 		assert(unescape(enc) == raw);
228 	}
229 	test("foo", "f_foo");
230 	test("foo.bar", "f_foo+2Ebar");
231 	test("foo+bar", "f_foo+2Bbar");
232 }
233