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 			database = Name of the database to use
56 			collection = Optional collection name to store the sessions in
57 	*/
58 	this(string url, string collection = "sessions")
59 	{
60 		import std.exception : enforce;
61 
62 		MongoClientSettings settings;
63 		enforce(parseMongoDBUrl(settings, url),
64 			"Failed to parse MongoDB URL.");
65 		auto db = connectMongoDB(settings).getDatabase(settings.database);
66 		m_sessions = db[collection];
67 	}
68 
69 	/** The duration without access after which a session expires.
70 	*/
71 	@property Duration expirationTime() const { return m_expirationTime; }
72 	/// ditto
73 	@property void expirationTime(Duration dur)
74 	{
75 		import std.typecons : tuple;
76 		IndexModel[1] index;
77 		index[0].add("time", 1);
78 		index[0].options.expireAfter = dur;
79 		m_sessions.createIndexes(index[]);
80 		m_expirationTime = dur;
81 	}
82 
83 	@property SessionStorageType storageType() const { return SessionStorageType.bson; }
84 
85 	Session create()
86 	{
87 		auto s = createSessionInstance();
88 		m_sessions.insertOne(SessionEntry(s.id, Clock.currTime(UTC())));
89 		return s;
90 	}
91 
92 	Session open(string id)
93 	{
94 		auto res = m_sessions.findAndModify(["_id": id], ["$set": ["time": Clock.currTime(UTC())]], ["_id": 1]);
95 		if (!res.isNull) return createSessionInstance(id);
96 		return Session.init;
97 	}
98 
99 	void set(string id, string name, Variant value)
100 	@trusted {
101 		m_sessions.updateOne(["_id": id], ["$set": [name.escape: value.get!Bson, "time": Clock.currTime(UTC()).serializeToBson]]);
102 	}
103 
104 	Variant get(string id, string name, lazy Variant defaultVal)
105 	@trusted {
106 		auto f = name.escape;
107 		FindOptions options;
108 		options.projection = Bson([f: Bson(1)]);
109 		auto r = m_sessions.findOne(["_id": id], options);
110 		if (r.isNull) return defaultVal;
111 		auto v = r.tryIndex(f);
112 		if (v.isNull) return defaultVal;
113 		return Variant(v.get);
114 	}
115 
116 	bool isKeySet(string id, string key)
117 	{
118 		auto f = key.escape;
119 		FindOptions options;
120 		options.projection = Bson([f: Bson(1)]);
121 		auto r = m_sessions.findOne(["_id": id], options);
122 		if (r.isNull) return false;
123 		return !r.tryIndex(f).isNull;
124 	}
125 
126 	void remove(string id, string key)
127 	{
128 		m_sessions.updateOne(["_id": id], ["$unset": [key.escape: 1]]);
129 	}
130 
131 	void destroy(string id)
132 	{
133 		m_sessions.deleteOne(["_id": id]);
134 	}
135 
136 	int iterateSession(string id, scope int delegate(string key) @safe del)
137 	{
138 		import std.algorithm.searching : startsWith;
139 
140 		auto r = m_sessions.findOne(["_id": id]);
141 		foreach (k, _; r.byKeyValue) {
142 			if (k.startsWith("f_")) {
143 				auto f = k.unescape;
144 				if (auto ret = del(f))
145 					return ret;
146 			}
147 		}
148 		return 0;
149 	}
150 
151 	private static struct SessionEntry {
152 		string _id;
153 		SysTime time;
154 	}
155 }
156 
157 
158 private string escape(string field_name)
159 @safe {
160 	import std.array : appender;
161 	import std.format : formattedWrite;
162 
163 	auto ret = appender!string;
164 	ret.reserve(field_name.length + 2);
165 	ret.put("f_");
166 	foreach (char ch; field_name) {
167 		switch (ch) {
168 			default:
169 				ret.formattedWrite("+%02X", cast(int)ch);
170 				break;
171 			case 'a': .. case 'z':
172 			case 'A': .. case 'Z':
173 			case '0': .. case '9':
174 			case '_', '-':
175 				ret.put(ch);
176 				break;
177 		}
178 	}
179 	return ret.data;
180 }
181 
182 private string unescape(string key)
183 @safe {
184 	import std.algorithm.searching : startsWith;
185 	import std.array : appender;
186 	import std.conv : to;
187 
188 	assert(key.startsWith("f_"));
189 	key = key[2 .. $];
190 	auto ret = appender!string;
191 	ret.reserve(key.length);
192 	while (key.length) {
193 		if (key[0] == '+') {
194 			ret.put(cast(char)key[1 .. 3].to!int(16));
195 			key = key[3 .. $];
196 		} else {
197 			ret.put(key[0]);
198 			key = key[1 .. $];
199 		}
200 	}
201 	return ret.data;
202 }
203 
204 @safe unittest {
205 	void test(string raw, string enc) {
206 		assert(escape(raw) == enc);
207 		assert(unescape(enc) == raw);
208 	}
209 	test("foo", "f_foo");
210 	test("foo.bar", "f_foo+2Ebar");
211 	test("foo+bar", "f_foo+2Bbar");
212 }
213