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