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.insert(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.update(["_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 		auto r = m_sessions.findOne(["_id": id], [f: 1]);
108 		if (r.isNull) return defaultVal;
109 		auto v = r.tryIndex(f);
110 		if (v.isNull) return defaultVal;
111 		return Variant(v.get);
112 	}
113 
114 	bool isKeySet(string id, string key)
115 	{
116 		auto f = key.escape;
117 		auto r = m_sessions.findOne(["_id": id], [f: 1]);
118 		if (r.isNull) return false;
119 		return !r.tryIndex(f).isNull;
120 	}
121 
122 	void remove(string id, string key)
123 	{
124 		m_sessions.update(["_id": id], ["$unset": [key.escape: 1]]);
125 	}
126 
127 	void destroy(string id)
128 	{
129 		m_sessions.remove(["_id": id]);
130 	}
131 
132 	int iterateSession(string id, scope int delegate(string key) @safe del)
133 	{
134 		import std.algorithm.searching : startsWith;
135 
136 		auto r = m_sessions.findOne(["_id": id]);
137 		foreach (k, _; r.byKeyValue) {
138 			if (k.startsWith("f_")) {
139 				auto f = k.unescape;
140 				if (auto ret = del(f))
141 					return ret;
142 			}
143 		}
144 		return 0;
145 	}
146 
147 	private static struct SessionEntry {
148 		string _id;
149 		SysTime time;
150 	}
151 }
152 
153 
154 private string escape(string field_name)
155 @safe {
156 	import std.array : appender;
157 	import std.format : formattedWrite;
158 
159 	auto ret = appender!string;
160 	ret.reserve(field_name.length + 2);
161 	ret.put("f_");
162 	foreach (char ch; field_name) {
163 		switch (ch) {
164 			default:
165 				ret.formattedWrite("+%02X", cast(int)ch);
166 				break;
167 			case 'a': .. case 'z':
168 			case 'A': .. case 'Z':
169 			case '0': .. case '9':
170 			case '_', '-':
171 				ret.put(ch);
172 				break;
173 		}
174 	}
175 	return ret.data;
176 }
177 
178 private string unescape(string key)
179 @safe {
180 	import std.algorithm.searching : startsWith;
181 	import std.array : appender;
182 	import std.conv : to;
183 
184 	assert(key.startsWith("f_"));
185 	key = key[2 .. $];
186 	auto ret = appender!string;
187 	ret.reserve(key.length);
188 	while (key.length) {
189 		if (key[0] == '+') {
190 			ret.put(cast(char)key[1 .. 3].to!int(16));
191 			key = key[3 .. $];
192 		} else {
193 			ret.put(key[0]);
194 			key = key[1 .. $];
195 		}
196 	}
197 	return ret.data;
198 }
199 
200 @safe unittest {
201 	void test(string raw, string enc) {
202 		assert(escape(raw) == enc);
203 		assert(unescape(enc) == raw);
204 	}
205 	test("foo", "f_foo");
206 	test("foo.bar", "f_foo+2Ebar");
207 	test("foo+bar", "f_foo+2Bbar");
208 }
209