From 5fddedac0477861b7723a6f0bce9d3e3c816c3f9 Mon Sep 17 00:00:00 2001
From: Bruce Toll <btoll@dhsus.com>
Date: Sat, 30 May 2020 16:42:30 -0400
Subject: [PATCH 1/4] Add framework for weakSessionId support

Provide an alternative weakSessionId associated with each WebSession
that can be used to replace application sessionIds in contexts where
only limited access to the application session is required, e.g. to
lookup private WResources.

This commit assigns an independent weakSessionId to each WebSession
using generateSessionId and tracks this weakSessionId along with the
existing sessionId in the SessionMap. To accommodate the extra index,
the std::map is replaced with a boost::multi_index_container.

The commit also adds some extra logging for the weakSessionIds and
some additional checks that the assigned Ids are globally unique.
---
 src/Wt/Test/WTestEnvironment.C |   2 +-
 src/web/WebController.C        | 135 ++++++++++++++++++++++++++-------
 src/web/WebController.h        |  44 ++++++++++-
 src/web/WebSession.C           |  28 ++++---
 src/web/WebSession.h           |  20 +++--
 5 files changed, 184 insertions(+), 45 deletions(-)

diff --git a/src/Wt/Test/WTestEnvironment.C b/src/Wt/Test/WTestEnvironment.C
index 2d6a2644..1a6995cf 100644
--- a/src/Wt/Test/WTestEnvironment.C
+++ b/src/Wt/Test/WTestEnvironment.C
@@ -85,7 +85,7 @@ WTestEnvironment::WTestEnvironment(Configuration *configuration,
 
 void WTestEnvironment::init(EntryPointType type)
 {
-  session_ = new WebSession(controller_, "testwtd", type, "", 0, this);
+  session_ = new WebSession(controller_, "testwtd", "weaktestwtd", type, "", 0, this);
   theSession_.reset(session_);
 
 #ifndef WT_TARGET_JAVA
diff --git a/src/web/WebController.C b/src/web/WebController.C
index f70cfc5f..092e794f 100644
--- a/src/web/WebController.C
+++ b/src/web/WebController.C
@@ -115,7 +115,7 @@ void WebController::shutdown()
 
       for (SessionMap::iterator i = sessions_.begin(); i != sessions_.end();
 	   ++i)
-	sessionList.push_back(i->second);
+	sessionList.push_back(i->session());
 
       sessions_.clear();
 
@@ -163,7 +163,7 @@ std::vector<std::string> WebController::sessions()
 #endif
   std::vector<std::string> sessionIds;
   for (SessionMap::const_iterator i = sessions_.begin(); i != sessions_.end(); ++i) {
-    sessionIds.push_back(i->first);
+    sessionIds.push_back(i->sessionId());
   }
   return sessionIds;
 }
@@ -181,7 +181,7 @@ bool WebController::expireSessions()
 #endif // WT_THREADED
 
     for (SessionMap::iterator i = sessions_.begin(); i != sessions_.end();) {
-      std::shared_ptr<WebSession> session = i->second;
+      std::shared_ptr<WebSession> session = i->session();
 
       int diff = session->expireTime() - now;
 
@@ -214,13 +214,17 @@ bool WebController::expireSessions()
   return result;
 }
 
+/*
+ * addSession is called from WTestEnvironment w/hard-coded sessionId and weakSessionId
+ * ...so we can avoid checking that these ids are distinct from one another
+ */
 void WebController::addSession(const std::shared_ptr<WebSession>& session)
 {
 #ifdef WT_THREADED
   std::unique_lock<std::recursive_mutex> lock(mutex_);
 #endif // WT_THREADED
 
-  sessions_[session->sessionId()] = session;
+  sessions_.insert(SessionEntry(session->sessionId(), session->weakSessionId(), session));
 }
 
 void WebController::removeSession(const std::string& sessionId)
@@ -234,7 +238,7 @@ void WebController::removeSession(const std::string& sessionId)
   SessionMap::iterator i = sessions_.find(sessionId);
   if (i != sessions_.end()) {
     ++zombieSessions_;
-    if (i->second->env().ajax())
+    if (i->session()->env().ajax())
       --ajaxSessions_;
     else
       --plainHtmlSessions_;
@@ -466,6 +470,21 @@ bool WebController::requestDataReceived(WebRequest *request,
       return false;
 
     std::string sessionId = *wtdE;
+    /*
+     * This could be a weak sessionId, but we need to post event to
+     * a normal sessionId. So, look it up as a weakSessionId and replace
+     * with the corresponding sessionId, if found...
+     */
+    {
+#ifdef WT_THREADED
+      std::unique_lock<std::recursive_mutex> lock(mutex_);
+      SessionMap::index<WeakSessionIdTag>::type::iterator iw =
+        sessions_.get<WeakSessionIdTag>().find(sessionId);
+      if (iw != sessions_.get<WeakSessionIdTag>().end()) {
+        sessionId = iw->sessionId();
+      }
+#endif // WT_THREADED
+    }
 
     ApplicationEvent event(sessionId,
 			   std::bind(&WebController::updateResourceProgress,
@@ -525,8 +544,8 @@ bool WebController::handleApplicationEvent(const ApplicationEvent& event)
 
     SessionMap::iterator i = sessions_.find(event.sessionId);
 
-    if (i != sessions_.end() && !i->second->dead())
-      session = i->second;
+    if (i != sessions_.end() && !i->session()->dead())
+      session = i->session();
   }
 
   if (!session) {
@@ -637,7 +656,7 @@ void WebController::handleRequest(WebRequest *request)
     return;
   }
 
-  std::string sessionId;
+  std::string sessionId, weakSessionId;
 
   /*
    * Get session from request.
@@ -691,14 +710,25 @@ void WebController::handleRequest(WebRequest *request)
 
     SessionMap::iterator i = sessions_.find(sessionId);
 
+    // Not a valid regular sessionId, perhaps it is a weakSessionId (used for resources)?
+    if (i == sessions_.end()) {
+      SessionMap::index<WeakSessionIdTag>::type::iterator iw =
+        sessions_.get<WeakSessionIdTag>().find(sessionId);
+
+      // if matched a weakSessionId, set i iterator to point at SessionElement
+      if (iw != sessions_.get<WeakSessionIdTag>().end()) {
+        i = sessions_.project<SessionIdTag>(iw);
+      }
+    }
+
     Configuration::SessionTracking sessionTracking = configuration().sessionTracking();
 
-    if (i == sessions_.end() || i->second->dead() ||
+    if (i == sessions_.end() || i->session()->dead() ||
         (sessionTracking == Configuration::Combined &&
-	 (multiSessionCookie.empty() || multiSessionCookie != i->second->multiSessionId()))) {
+	 (multiSessionCookie.empty() || multiSessionCookie != i->session()->multiSessionId()))) {
       try {
         if (sessionTracking == Configuration::Combined &&
-            i != sessions_.end() && !i->second->dead()) {
+            i != sessions_.end() && !i->session()->dead()) {
           if (!request->headerValue("Cookie")) {
             LOG_ERROR_S(&server_, "Valid session id: " << sessionId << ", but "
                         "no cookie received (expecting multi session cookie)");
@@ -718,18 +748,19 @@ void WebController::handleRequest(WebRequest *request)
         }
 
 	if (singleSessionId_.empty()) {
-	  do {
-	    sessionId = conf_.generateSessionId();
-	    if (!conf_.registerSessionId(std::string(), sessionId))
-	      sessionId.clear();
-	  } while (sessionId.empty());
+          if (!findAvailableSessionIds("", sessionId, weakSessionId)) {
+            LOG_ERROR_S(&server_, "Could not allocate session ids. Consider increasing session-id-length");
+            request->setStatus(500);
+            request->flush(WebResponse::ResponseState::ResponseDone);
+            return;
+          }
 	}
 
 	std::string favicon = request->entryPoint_->favicon();
 	if (favicon.empty())
 	  conf_.readConfigurationProperty("favicon", favicon);
 
-	session.reset(new WebSession(this, sessionId,
+	session.reset(new WebSession(this, sessionId, weakSessionId,
 				     request->entryPoint_->type(),
 				     favicon, request));
 
@@ -746,7 +777,7 @@ void WebController::handleRequest(WebRequest *request)
 			     + " Path=" + session->env().deploymentPath()
 			     + "; httponly;" + (session->env().urlScheme() == "https" ? " secure;" : ""));
 
-	sessions_[sessionId] = session;
+        sessions_.insert(SessionEntry(sessionId, weakSessionId, session));
 	++plainHtmlSessions_;
       } catch (std::exception& e) {
 	LOG_ERROR_S(&server_, "could not create new session: " << e.what());
@@ -754,7 +785,7 @@ void WebController::handleRequest(WebRequest *request)
 	return;
       }
     } else {
-      session = i->second;
+      session = i->session();
     }
   }
 
@@ -797,6 +828,59 @@ EntryPointMatch WebController::getEntryPoint(WebRequest *request)
   return conf_.matchEntryPoint(scriptName, pathInfo, false);
 }
 
+/*
+ * If either sessionId or WeakSessionId in/out parameter is empty,
+ * replace it with a newly generated sessionId -- if one is available.
+ * For a sessionId to be considered available, it must be:
+ *
+ * 1. unused in the SessionIdTag index to sessions_
+ * 2. unused in the WeakSessionIdTag index to sessions_
+ *
+ * For the SessionId, the registerSessionId method must also
+ * succeed (used by FCGI connector).
+ *
+ * Return true on success.
+ *
+ * NOTE: The WebController mutex_ must be held when calling this method
+ */
+bool WebController::findAvailableSessionIds(std::string registrationOldId,
+                                            std::string& sessionId,
+                                            std::string& weakSessionId)
+{
+  // With default length session ids, the likelihood of any retries is very small
+  for (int retry = 0; weakSessionId.empty() && retry < 10; ++retry) {
+    weakSessionId = conf_.generateSessionId();
+    if (sessions_.get<WeakSessionIdTag>().find(weakSessionId) !=
+        sessions_.get<WeakSessionIdTag>().end()) {
+      weakSessionId.clear();
+      continue;
+    }
+    if (sessions_.find(weakSessionId) != sessions_.end())
+      weakSessionId.clear();
+  }
+
+  for (int retry = 0; sessionId.empty() && retry < 10; ++retry) {
+    sessionId = conf_.generateSessionId();
+    if (sessionId == weakSessionId) {
+      sessionId.clear();
+      continue;
+    }
+    if (sessions_.get<WeakSessionIdTag>().find(sessionId) !=
+        sessions_.get<WeakSessionIdTag>().end()) {
+      sessionId.clear();
+      continue;
+    }
+    if (sessions_.find(sessionId) != sessions_.end()) {
+      sessionId.clear();
+      continue;
+    }
+    if (!conf_.registerSessionId(registrationOldId, sessionId))
+      sessionId.clear();
+  }
+
+  return !sessionId.empty() && !weakSessionId.empty();
+}
+
 std::string
 WebController::generateNewSessionId(const std::shared_ptr<WebSession>& session)
 {
@@ -804,14 +888,13 @@ WebController::generateNewSessionId(const std::shared_ptr<WebSession>& session)
   std::unique_lock<std::recursive_mutex> lock(mutex_);
 #endif // WT_THREADED  
 
-  std::string newSessionId;
-  do {
-    newSessionId = conf_.generateSessionId();
-    if (!conf_.registerSessionId(session->sessionId(), newSessionId))
-      newSessionId.clear();
-  } while (newSessionId.empty());
+  std::string newSessionId, newWeakSessionId;
+  if (!findAvailableSessionIds(session->sessionId(), newSessionId, newWeakSessionId)) {
+    throw WException(
+      "generateNewSessionId: out of session ids. Consider increasing session-id-length");
+  }
 
-  sessions_[newSessionId] = session;
+  sessions_.insert(SessionEntry(newSessionId, newWeakSessionId, session));
 
   SessionMap::iterator i = sessions_.find(session->sessionId());
   sessions_.erase(i);
diff --git a/src/web/WebController.h b/src/web/WebController.h
index 596ed6ff..dd926e5c 100644
--- a/src/web/WebController.h
+++ b/src/web/WebController.h
@@ -12,6 +12,11 @@
 #include <set>
 #include <map>
 
+#include <boost/multi_index_container.hpp>
+#include <boost/multi_index/hashed_index.hpp>
+#include <boost/multi_index/ordered_index.hpp>
+#include <boost/multi_index/member.hpp>
+
 #include <Wt/WDllDefs.h>
 #include <Wt/WServer.h>
 #include <Wt/WSocketNotifier.h>
@@ -154,6 +159,10 @@ public:
 
   std::string switchSession(WebSession *session,
 			    const std::string& newSessionId);
+
+  bool findAvailableSessionIds (std::string registrationOldId,
+                                std::string& sessionId,
+                                std::string& weakSessionId);
   std::string generateNewSessionId(const std::shared_ptr<WebSession>& session);
 
 private:
@@ -170,7 +179,40 @@ private:
 #endif // WT_THREADED
   std::set<std::string> uploadProgressUrls_;
 
-  typedef std::map<std::string, std::shared_ptr<WebSession> > SessionMap;
+  class SessionMap;
+  class SessionEntry {
+  public:
+    SessionEntry(std::string sessionId, std::string weakSessionId, std::shared_ptr<WebSession> session) :
+      sessionId_(sessionId), weakSessionId_(weakSessionId), session_(session) {}
+
+    std::string sessionId() const { return sessionId_; }
+    std::string weakSessionId() const { return weakSessionId_; }
+    std::shared_ptr<WebSession> session() const { return session_; }
+
+  private:
+    std::string sessionId_;
+    std::string weakSessionId_;
+    std::shared_ptr<WebSession> session_;
+
+  friend class SessionMap;
+  };
+
+  struct SessionIdTag { };
+  struct WeakSessionIdTag { };
+  class SessionMap : public boost::multi_index_container<
+    SessionEntry,
+    boost::multi_index::indexed_by<
+      boost::multi_index::ordered_unique<
+        boost::multi_index::tag<SessionIdTag>,
+        boost::multi_index::member<SessionEntry, std::string, &SessionEntry::sessionId_>
+      >,
+      boost::multi_index::hashed_unique<
+        boost::multi_index::tag<WeakSessionIdTag>,
+        boost::multi_index::member<SessionEntry, std::string, &SessionEntry::weakSessionId_>
+      >
+    >
+  >{ };
+
   SessionMap sessions_;
 
 #ifdef WT_THREADED
diff --git a/src/web/WebSession.C b/src/web/WebSession.C
index 8a4e0cb2..45e93205 100644
--- a/src/web/WebSession.C
+++ b/src/web/WebSession.C
@@ -96,6 +96,7 @@ static WebSession::Handler * threadHandler_ = nullptr;
 
 WebSession::WebSession(WebController *controller,
 		       const std::string& sessionId,
+		       const std::string& weakSessionId,
 		       EntryPointType type,
 		       const std::string& favicon,
                        const WebRequest *request,
@@ -104,6 +105,7 @@ WebSession::WebSession(WebController *controller,
     favicon_(favicon),
     state_(State::JustCreated),
     sessionId_(sessionId),
+    weakSessionId_(weakSessionId),
     sessionIdChanged_(false),
     sessionIdCookieChanged_(false),
     sessionIdInUrl_(false),
@@ -160,8 +162,8 @@ WebSession::WebSession(WebController *controller,
   }
 
 #ifndef WT_TARGET_JAVA
-  LOG_INFO("session created (#sessions = " <<
-	   (controller_->sessionCount() + 1) << ")");
+  LOG_INFO("session created (#sessions = " << (controller_->sessionCount() + 1)
+           << "), sessionId: " << sessionId_ << ", weakSessionId: " << weakSessionId_);
 
   expire_ = Time() + 60*1000;
 #endif // WT_TARGET_JAVA
@@ -365,9 +367,11 @@ void WebSession::setState(State state, int timeout)
   }
 }
 
-std::string WebSession::sessionQuery() const
+std::string WebSession::sessionQuery(SessionOption sessionOption) const
 {
-  std::string result ="?wtd=" + DomElement::urlEncodeS(sessionId_);
+  const std::string& sessionId =
+    (sessionOption == SessionOption::UseSessionId ?  sessionId_ : weakSessionId_);
+  std::string result ="?wtd=" + DomElement::urlEncodeS(sessionId);
   if (type() == EntryPointType::WidgetSet)
     result += "&wtt=widgetset";
   return result;
@@ -587,12 +591,14 @@ std::string WebSession::makeAbsoluteUrl(const std::string& url) const
   }
 }
 
-std::string WebSession::mostRelativeUrl(const std::string& internalPath) const
+std::string WebSession::mostRelativeUrl(const std::string& internalPath,
+                                        SessionOption sessionOption) const
 {
-  return appendSessionQuery(bookmarkUrl(internalPath));
+  return appendSessionQuery(bookmarkUrl(internalPath), sessionOption);
 }
 
-std::string WebSession::appendSessionQuery(const std::string& url) const
+std::string WebSession::appendSessionQuery(const std::string& url,
+                                           SessionOption sessionOption) const
 {
   std::string result = url;
   
@@ -602,11 +608,11 @@ std::string WebSession::appendSessionQuery(const std::string& url) const
   std::size_t questionPos = result.find('?');
 
   if (questionPos == std::string::npos)
-    result += sessionQuery();
+    result += sessionQuery(sessionOption);
   else if (questionPos == result.length() - 1)
-    result += sessionQuery().substr(1);
+    result += sessionQuery(sessionOption).substr(1);
   else
-    result += '&' + sessionQuery().substr(1);
+    result += '&' + sessionQuery(sessionOption).substr(1);
 
 #ifndef WT_TARGET_JAVA
   return result;
@@ -3061,7 +3067,7 @@ void WebSession::generateNewSessionId()
   sessionId_ = controller_->generateNewSessionId(shared_from_this());
   sessionIdChanged_ = true;
 
-  LOG_INFO("new session id for " << oldId);
+  LOG_INFO("new session id for: " << oldId << ", new weak session id: " << weakSessionId_);
 
   if (!useUrlRewriting()) {
     std::string cookieName = env_->deploymentPath();
diff --git a/src/web/WebSession.h b/src/web/WebSession.h
index bdcfb561..8d08ecd4 100644
--- a/src/web/WebSession.h
+++ b/src/web/WebSession.h
@@ -62,7 +62,8 @@ public:
     Dead
   };
 
-  WebSession(WebController *controller, const std::string& sessionId,
+  WebSession(WebController *controller,
+	     const std::string& sessionId, const std::string& weakSessionId,
 	     EntryPointType type, const std::string& favicon,
 	     const WebRequest *request, WEnvironment *env = nullptr);
   ~WebSession();
@@ -80,6 +81,7 @@ public:
   std::string docType() const;
 
   std::string sessionId() const { return sessionId_; }
+  std::string weakSessionId() const { return weakSessionId_; }
   std::string multiSessionId() const { return multiSessionId_; }
   void setMultiSessionId(const std::string &multiSessionId);
 
@@ -141,15 +143,20 @@ public:
   void setPagePathInfo(const std::string& path);
   std::string pagePathInfo() const { return pagePathInfo_; }
 
+  enum class SessionOption {
+    UseSessionId,
+    UseWeakSessionId
+  };
   //    (http://www.bigapp.com/myapp/app.wt) ?wtd=ABCD
   // or (http://www.bigapp.com/myapp/) app.wt/path?wtd=ABCD
-  std::string mostRelativeUrl(const std::string& internalPath = std::string())
-    const;
+  std::string mostRelativeUrl(const std::string& internalPath = std::string(),
+    SessionOption sessionOption = SessionOption::UseSessionId) const;
 
   std::string appendInternalPath(const std::string& url,
 				 const std::string& internalPath) const;
 
-  std::string appendSessionQuery(const std::string& url) const;
+  std::string appendSessionQuery(const std::string& url,
+    SessionOption sessionOption = SessionOption::UseSessionId) const;
 
   std::string ajaxCanonicalUrl(const WebResponse& request) const;
 
@@ -294,7 +301,7 @@ private:
   std::string favicon_;
   State state_;
 
-  std::string sessionId_, sessionIdCookie_, multiSessionId_;
+  std::string sessionId_, weakSessionId_, sessionIdCookie_, multiSessionId_;
   bool sessionIdChanged_, sessionIdCookieChanged_, sessionIdInUrl_;
 
   WebController *controller_;
@@ -369,7 +376,8 @@ private:
   void init(const WebRequest& request);
   bool start(WebResponse *response);
 
-  std::string sessionQuery() const;
+  std::string sessionQuery(
+          SessionOption sessionOption = SessionOption::UseSessionId) const;
   void flushBootStyleResponse();
   void changeInternalPath(const std::string& path, WebResponse *response);
 
-- 
2.23.2

