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