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