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