NAWA 0.9
Web Application Framework for C++
Session.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2019-2022 Tobias Flaig.
3 *
4 * This file is part of nawa.
5 *
6 * nawa is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License,
8 * version 3, as published by the Free Software Foundation.
9 *
10 * nawa is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with nawa. If not, see <https://www.gnu.org/licenses/>.
17 */
18
24#include <mutex>
25#include <nawa/Exception.h>
28#include <nawa/util/crypto.h>
29#include <random>
30
31using namespace nawa;
32using namespace std;
33
34namespace {
38 struct SessionData {
39 mutex dLock;
40 mutex eLock;
41 unordered_map<string, any> data;
42 time_t expires;
43 const string sourceIP;
48 SessionData() : expires(0) {}
49
54 explicit SessionData(string sIP) : expires(0), sourceIP(std::move(sIP)) {}
55 };
56
57 mutex gLock;
61 unordered_map<string, shared_ptr<SessionData>> sessionData;
62
68 string generateID(string const& remoteAddress) {
69 stringstream base;
70
71 // Add 2 ints from random_device (should be in fact /dev/urandom), giving us (in general) 64 bits of entropy
72 random_device rd;
73 base << rd() << rd();
74
75 // Add client IP
76 base << remoteAddress;
77
78 // Calculate and return hex-formatted SHA1
79 return crypto::sha1(base.str(), true);
80 }
81
86 void collectGarbage() {
87 lock_guard<mutex> lockGuard(gLock);
88 // no increment in for statement as we want to remove elements
89 for (auto it = sessionData.cbegin(); it != sessionData.cend();) {
90 bool toDelete = false;
91 {
92 lock_guard<mutex> eGuard(it->second->eLock);
93 toDelete = (it->second->expires < time(nullptr));
94 }
95 if (toDelete) {
96 it = sessionData.erase(it);
97 } else {
98 ++it;
99 }
100 }
101 }
102}// namespace
103
104struct Session::Data {
105 nawa::Connection& connection;
111 std::shared_ptr<SessionData> currentData;
112 std::string currentID;
113 std::string cookieName;
115 explicit Data(Connection& connection) : connection(connection) {}
116};
117
119
121 data = make_unique<Data>(connection);
122
123 // session autostart cannot happen here yet, as connection.config is not yet available (dangling)
124 // thus, it will be triggered by the Connection constructor
125}
126
127std::string Session::start(std::string sessionId, std::optional<unsigned long> keepalive) {
128 // if session already started, do not start it again
129 if (established()) {
130 return data->currentID;
131 }
132
133 // session duration
134 unsigned long sessionKeepalive = 1800;
135 if (keepalive) {
136 sessionKeepalive = *keepalive;
137 } else {
138 auto sessionKStr = data->connection.config()[{"session", "keepalive"}];
139 if (!sessionKStr.empty()) {
140 try {
141 sessionKeepalive = stoul(sessionKStr);
142 } catch (invalid_argument& e) {
143 sessionKeepalive = 1800;
144 }
145 }
146 }
147
148 if (!sessionId.empty()) {
149 // check for validity
150 // global data map may be accessed concurrently by different threads
151 lock_guard<mutex> lockGuard(gLock);
152 if (sessionData.count(sessionId) == 1) {
153 // read validate_ip setting from config (needed a few lines later)
154 auto sessionValidateIP = data->connection.config()[{"session", "validate_ip"}];
155 // session already expired?
156 if (sessionData.at(sessionId)->expires <= time(nullptr)) {
157 sessionData.erase(sessionId);
158 }
159 // validate_ip enabled in NAWA config and IP mismatch?
160 else if ((sessionValidateIP == "strict" || sessionValidateIP == "lax") &&
161 sessionData.at(sessionId)->sourceIP != data->connection.request().env()["REMOTE_ADDR"]) {
162 if (sessionValidateIP == "strict") {
163 // in strict mode, session has to be invalidated
164 sessionData.erase(sessionId);
165 }
166 }
167 // session is valid
168 else {
169 data->currentData = sessionData.at(sessionId);
170 // reset expiry
171 lock_guard<mutex> currentLock(data->currentData->eLock);
172 data->currentData->expires = time(nullptr) + sessionKeepalive;
173 }
174 }
175 }
176 // if currentData not yet set (sessionCookieStr empty or invalid) -> initiate new session
177 if (data->currentData.use_count() < 1) {
178 // generate new session ID string (and check for duplicate - should not really occur)
179 lock_guard<mutex> lockGuard(gLock);
180 do {
181 sessionId = generateID(data->connection.request().env()["REMOTE_ADDR"]);
182 } while (sessionData.count(sessionId) > 0);
183 data->currentData = make_shared<SessionData>(data->connection.request().env()["REMOTE_ADDR"]);
184 data->currentData->expires = time(nullptr) + sessionKeepalive;
185 sessionData[sessionId] = data->currentData;
186 }
187
188 // save the ID so we can invalidate the session
189 data->currentID = sessionId;
190
191 // run garbage collection in 1/x of invocations
192 unsigned long divisor;
193 try {
194 auto divisorStr = data->connection.config()[{"session", "gc_divisor"}];
195 if (!divisorStr.empty()) {
196 divisor = stoul(divisorStr);
197 } else {
198 divisor = 100;
199 }
200 } catch (invalid_argument const& e) {
201 divisor = 100;
202 }
203 random_device rd;
204 if (rd() % divisor == 0) {
205 collectGarbage();
206 }
207
208 return sessionId;
209}
210
211void Session::start(Cookie properties) {
212 // if session already started, do not start it again
213 if (established())
214 return;
215
216 // get name of session cookie from config
217 data->cookieName = data->connection.config()[{"session", "cookie_name"}];
218 if (data->cookieName.empty()) {
219 data->cookieName = "SESSION";
220 }
221
222 // session duration
223 unsigned long sessionKeepalive = 1800;
224 if (properties.maxAge()) {
225 sessionKeepalive = *properties.maxAge();
226 } else {
227 auto sessionKStr = data->connection.config()[{"session", "keepalive"}];
228 if (!sessionKStr.empty()) {
229 try {
230 sessionKeepalive = stoul(sessionKStr);
231 } catch (invalid_argument& e) {
232 sessionKeepalive = 1800;
233 }
234 }
235 }
236
237 // the session ID may be given in a session cookie, if not, the string will be empty
238 // Session::start will use the session, if present, and return a valid session ID
239 string sessionId = start(data->connection.request().cookie()[data->cookieName], sessionKeepalive);
240
241 // set the response cookie and its properties according to the Cookie parameter or the NAWA config
242 string cookieExpiresStr;
243 if (properties.expires() || data->connection.config()[{"session", "cookie_expires"}] != "off") {
244 properties.expires(time(nullptr) + sessionKeepalive)
245 .maxAge(sessionKeepalive);
246 } else {
247 // we need to unset the maxAge value if it should not be used for the cookie
248 properties.maxAge(nullopt);
249 }
250
251 if (!properties.secure() && data->connection.config()[{"session", "cookie_secure"}] != "off") {
252 properties.secure(true);
253 }
254 if (!properties.httpOnly() && data->connection.config()[{"session", "cookie_httponly"}] != "off") {
255 properties.httpOnly(true);
256 }
257 if (properties.sameSite() == Cookie::SameSite::OFF) {
258 auto sessionSameSite = data->connection.config()[{"session", "cookie_samesite"}];
259 if (sessionSameSite == "lax") {
261 } else if (sessionSameSite != "off") {
263 }
264 }
265
266 // save the ID so we can invalidate the session
267 data->currentID = sessionId;
268
269 // set the content to the session ID and queue the cookie
270 properties.content(sessionId);
271 data->connection.setCookie(data->cookieName, properties);
272}
273
275 return (data->currentData.use_count() > 0);
276}
277
278bool Session::isSet(std::string const& key) const {
279 if (established()) {
280 lock_guard<mutex> lockGuard(data->currentData->dLock);
281 return (data->currentData->data.count(key) == 1);
282 }
283 return false;
284}
285
286std::any Session::operator[](std::string const& key) const {
287 if (established()) {
288 lock_guard<mutex> lockGuard(data->currentData->dLock);
289 if (data->currentData->data.count(key) == 1) {
290 return data->currentData->data.at(key);
291 }
292 }
293 return {};
294}
295
296// doxygen bug requires std:: here
297void Session::set(std::string key, std::any const& value) {
298 if (!established()) {
299 throw Exception(__PRETTY_FUNCTION__, 1, "Session not established.");
300 }
301 lock_guard<mutex> lockGuard(data->currentData->dLock);
302 data->currentData->data[std::move(key)] = value;
303}
304
305void Session::unset(std::string const& key) {
306 if (!established()) {
307 throw Exception(__PRETTY_FUNCTION__, 1, "Session not established.");
308 }
309 lock_guard<mutex> lockGuard(data->currentData->dLock);
310 data->currentData->data.erase(key);
311}
312
314
315 // do nothing if no session has been established
316 if (!established())
317 return;
318
319 // reset currentData pointer, this will also make established() return false
320 data->currentData.reset();
321
322 // erase this session from the data map
323 {
324 lock_guard<mutex> lockGuard(gLock);
325 sessionData.erase(data->currentID);
326 }
327
328 // unset the session cookie, so that a new session can be started
329 data->connection.unsetCookie(data->cookieName);
330}
331
332std::string Session::getID() const {
333 return established() ? data->currentID : string();
334}
335
336void Session::destroy() {
337 sessionData.clear();
338}
Response object to be passed back to NAWA and accessor to the request.
Exception class that can be used by apps to catch errors resulting from nawa function calls.
Class for managing sessions and getting and setting connection-independent session data.
bool & secure() noexcept
std::optional< time_t > & expires() noexcept
SameSite & sameSite() noexcept
std::string & content() noexcept
bool & httpOnly() noexcept
std::optional< unsigned long > & maxAge() noexcept
std::any operator[](std::string const &key) const
Definition: Session.cpp:286
void set(std::string key, const std::any &value)
Definition: Session.cpp:297
std::string start(std::string sessionId, std::optional< unsigned long > keepalive=std::nullopt)
Definition: Session.cpp:127
void invalidate()
Definition: Session.cpp:313
bool established() const
Definition: Session.cpp:274
std::string getID() const
Definition: Session.cpp:332
void unset(std::string const &key)
Definition: Session.cpp:305
bool isSet(std::string const &key) const
Definition: Session.cpp:278
A bunch of useful cryptographic functions (esp. hashing), acting as a wrapper to C crypto libraries.
#define NAWA_DEFAULT_DESTRUCTOR_IMPL(Class)
Definition: macros.h:36
std::string sha1(std::string const &input, bool hex=true)
Definition: crypto.cpp:33
Definition: AppInit.h:31