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>
27 #include <nawa/session/Session.h>
28 #include <nawa/util/crypto.h>
29 #include <random>
30 
31 using namespace nawa;
32 using namespace std;
33 
34 namespace {
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(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 
104 struct 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 
120 Session::Session(Connection& connection) {
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 
127 void Session::start(Cookie properties) {
128 
129  // if session already started, do not start it again
130  if (established())
131  return;
132 
133  // get name of session cookie from config
134  data->cookieName = data->connection.config()[{"session", "cookie_name"}];
135  if (data->cookieName.empty()) {
136  data->cookieName = "SESSION";
137  }
138 
139  // session duration
140  unsigned long sessionKeepalive = 1800;
141  if (properties.maxAge()) {
142  sessionKeepalive = *properties.maxAge();
143  } else {
144  auto sessionKStr = data->connection.config()[{"session", "keepalive"}];
145  if (!sessionKStr.empty()) {
146  try {
147  sessionKeepalive = stoul(sessionKStr);
148  } catch (invalid_argument& e) {
149  sessionKeepalive = 1800;
150  }
151  }
152  }
153 
154  // check whether client has submitted a session cookie
155  auto sessionCookieStr = data->connection.request().cookie()[data->cookieName];
156  if (!sessionCookieStr.empty()) {
157  // check for validity
158  // global data map may be accessed concurrently by different threads
159  lock_guard<mutex> lockGuard(gLock);
160  if (sessionData.count(sessionCookieStr) == 1) {
161  // read validate_ip setting from config (needed a few lines later)
162  auto sessionValidateIP = data->connection.config()[{"session", "validate_ip"}];
163  // session already expired?
164  if (sessionData.at(sessionCookieStr)->expires <= time(nullptr)) {
165  sessionData.erase(sessionCookieStr);
166  }
167  // validate_ip enabled in NAWA config and IP mismatch?
168  else if ((sessionValidateIP == "strict" || sessionValidateIP == "lax") &&
169  sessionData.at(sessionCookieStr)->sourceIP != data->connection.request().env()["REMOTE_ADDR"]) {
170  if (sessionValidateIP == "strict") {
171  // in strict mode, session has to be invalidated
172  sessionData.erase(sessionCookieStr);
173  }
174  }
175  // session is valid
176  else {
177  data->currentData = sessionData.at(sessionCookieStr);
178  // reset expiry
179  lock_guard<mutex> currentLock(data->currentData->eLock);
180  data->currentData->expires = time(nullptr) + sessionKeepalive;
181  }
182  }
183  }
184  // if currentData not yet set (sessionCookieStr empty or invalid) -> initiate new session
185  if (data->currentData.use_count() < 1) {
186  // generate new session ID string (and check for duplicate - should not really occur)
187  lock_guard<mutex> lockGuard(gLock);
188  do {
189  sessionCookieStr = generateID(data->connection.request().env()["REMOTE_ADDR"]);
190  } while (sessionData.count(sessionCookieStr) > 0);
191  data->currentData = make_shared<SessionData>(data->connection.request().env()["REMOTE_ADDR"]);
192  data->currentData->expires = time(nullptr) + sessionKeepalive;
193  sessionData[sessionCookieStr] = data->currentData;
194  }
195 
196  // set the response cookie and its properties according to the Cookie parameter or the NAWA config
197  string cookieExpiresStr;
198  if (properties.expires() || data->connection.config()[{"session", "cookie_expires"}] != "off") {
199  properties.expires(time(nullptr) + sessionKeepalive)
200  .maxAge(sessionKeepalive);
201  } else {
202  // we need to unset the maxAge value if it should not be used for the cookie
203  properties.maxAge(nullopt);
204  }
205 
206  if (!properties.secure() && data->connection.config()[{"session", "cookie_secure"}] != "off") {
207  properties.secure(true);
208  }
209  if (!properties.httpOnly() && data->connection.config()[{"session", "cookie_httponly"}] != "off") {
210  properties.httpOnly(true);
211  }
212  if (properties.sameSite() == Cookie::SameSite::OFF) {
213  auto sessionSameSite = data->connection.config()[{"session", "cookie_samesite"}];
214  if (sessionSameSite == "lax") {
215  properties.sameSite(Cookie::SameSite::LAX);
216  } else if (sessionSameSite != "off") {
218  }
219  }
220 
221  // save the ID so we can invalidate the session
222  data->currentID = sessionCookieStr;
223 
224  // set the content to the session ID and queue the cookie
225  properties.content(sessionCookieStr);
226  data->connection.setCookie(data->cookieName, properties);
227 
228  // run garbage collection in 1/x of invocations
229  unsigned long divisor;
230  try {
231  auto divisorStr = data->connection.config()[{"session", "gc_divisor"}];
232  if (!divisorStr.empty()) {
233  divisor = stoul(divisorStr);
234  } else {
235  divisor = 100;
236  }
237  } catch (invalid_argument const& e) {
238  divisor = 100;
239  }
240  random_device rd;
241  if (rd() % divisor == 0) {
242  collectGarbage();
243  }
244 }
245 
246 bool Session::established() const {
247  return (data->currentData.use_count() > 0);
248 }
249 
250 bool Session::isSet(string const& key) const {
251  if (established()) {
252  lock_guard<mutex> lockGuard(data->currentData->dLock);
253  return (data->currentData->data.count(key) == 1);
254  }
255  return false;
256 }
257 
258 any Session::operator[](string const& key) const {
259  if (established()) {
260  lock_guard<mutex> lockGuard(data->currentData->dLock);
261  if (data->currentData->data.count(key) == 1) {
262  return data->currentData->data.at(key);
263  }
264  }
265  return {};
266 }
267 
268 // doxygen bug requires std:: here
269 void Session::set(std::string key, std::any const& value) {
270  if (!established()) {
271  throw Exception(__PRETTY_FUNCTION__, 1, "Session not established.");
272  }
273  lock_guard<mutex> lockGuard(data->currentData->dLock);
274  data->currentData->data[move(key)] = value;
275 }
276 
277 void Session::unset(string const& key) {
278  if (!established()) {
279  throw Exception(__PRETTY_FUNCTION__, 1, "Session not established.");
280  }
281  lock_guard<mutex> lockGuard(data->currentData->dLock);
282  data->currentData->data.erase(key);
283 }
284 
286 
287  // do nothing if no session has been established
288  if (!established())
289  return;
290 
291  // reset currentData pointer, this will also make established() return false
292  data->currentData.reset();
293 
294  // erase this session from the data map
295  {
296  lock_guard<mutex> lockGuard(gLock);
297  sessionData.erase(data->currentID);
298  }
299 
300  // unset the session cookie, so that a new session can be started
301  data->connection.unsetCookie(data->cookieName);
302 }
303 
304 string Session::getID() const {
305  return established() ? data->currentID : string();
306 }
307 
308 void Session::destroy() {
309  sessionData.clear();
310 }
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.
SameSite & sameSite() noexcept
std::string & content() noexcept
bool & httpOnly() noexcept
std::optional< time_t > & expires() noexcept
bool & secure() noexcept
std::optional< unsigned long > & maxAge() noexcept
std::string getID() const
Definition: Session.cpp:304
void start(nawa::Cookie properties=Cookie())
Definition: Session.cpp:127
void set(std::string key, const std::any &value)
Definition: Session.cpp:269
std::any operator[](std::string const &key) const
Definition: Session.cpp:258
void invalidate()
Definition: Session.cpp:285
bool established() const
Definition: Session.cpp:246
void unset(std::string const &key)
Definition: Session.cpp:277
bool isSet(std::string const &key) const
Definition: Session.cpp:250
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