[cpp-sp] branch main updated: WIP on cookie-backed cache.

Codeberg noreply at shibboleth.net
Thu Oct 1 17:47:02 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch main
in repository cpp-sp.

View the commit online:
https://codeberg.org/Shibboleth/cpp-sp/commit/d8ea7f55ae4fd756b7f273a6317d9b1daa65288f

The following commit(s) were added to refs/heads/main by this push:
     new d8ea7f55 WIP on cookie-backed cache.
d8ea7f55 is described below

commit d8ea7f55ae4fd756b7f273a6317d9b1daa65288f
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Thu Oct 1 13:46:49 2026 -0400

    WIP on cookie-backed cache.
---
 Projects/vc22/shibsp.vcxproj                 |   1 +
 Projects/vc22/shibsp.vcxproj.filters         |   9 +-
 shibsp/Makefile.am                           |   1 +
 shibsp/RequestMapper.h                       |   2 +
 shibsp/impl/XMLRequestMapper.cpp             |   2 +
 shibsp/io/CookieManager.h                    |  45 ++-
 shibsp/io/impl/CookieManager.cpp             |  31 +-
 shibsp/session/SessionCache.h                |   3 +
 shibsp/session/impl/AbstractSessionCache.cpp |  30 +-
 shibsp/session/impl/CookieSessionCache.cpp   | 526 +++++++++++++++++++++++++++
 10 files changed, 622 insertions(+), 28 deletions(-)

diff --git a/Projects/vc22/shibsp.vcxproj b/Projects/vc22/shibsp.vcxproj
index 39de4daa..cc02517d 100644
--- a/Projects/vc22/shibsp.vcxproj
+++ b/Projects/vc22/shibsp.vcxproj
@@ -172,6 +172,7 @@
     <ClCompile Include="..\..\shibsp\remoting\impl\SecretSource.cpp" />
     <ClCompile Include="..\..\shibsp\remoting\impl\WinHTTPRemotingService.cpp" />
     <ClCompile Include="..\..\shibsp\session\impl\AbstractSessionCache.cpp" />
+    <ClCompile Include="..\..\shibsp\session\impl\CookieSessionCache.cpp" />
     <ClCompile Include="..\..\shibsp\session\impl\FilesystemSessionCache.cpp" />
     <ClCompile Include="..\..\shibsp\session\impl\MemorySessionCache.cpp" />
     <ClCompile Include="..\..\shibsp\session\impl\StorageServiceSessionCache.cpp" />
diff --git a/Projects/vc22/shibsp.vcxproj.filters b/Projects/vc22/shibsp.vcxproj.filters
index d385cc47..ccd33cce 100644
--- a/Projects/vc22/shibsp.vcxproj.filters
+++ b/Projects/vc22/shibsp.vcxproj.filters
@@ -344,9 +344,6 @@
     <ClCompile Include="..\..\shibsp\platform\iis\ModuleConfig.cpp">
       <Filter>Source Files\Platform\iis</Filter>
     </ClCompile>
-    <ClCompile Include="..\..\shibsp\session\impl\AbstractSessionCache.cpp">
-      <Filter>Source Files\Session</Filter>
-    </ClCompile>
     <ClCompile Include="..\..\shibsp\remoting\impl\AbstractHTTPRemotingService.cpp">
       <Filter>Source Files\Remoting</Filter>
     </ClCompile>
@@ -365,6 +362,12 @@
     <ClCompile Include="..\..\shibsp\session\impl\FilesystemSessionCache.cpp">
       <Filter>Source Files\Session</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\shibsp\session\impl\AbstractSessionCache.cpp">
+      <Filter>Source Files\Session</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\shibsp\session\impl\CookieSessionCache.cpp">
+      <Filter>Source Files\Session</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\shibsp\session\impl\MemorySessionCache.cpp">
       <Filter>Source Files\Session</Filter>
     </ClCompile>
diff --git a/shibsp/Makefile.am b/shibsp/Makefile.am
index d11aae09..434519fe 100644
--- a/shibsp/Makefile.am
+++ b/shibsp/Makefile.am
@@ -133,6 +133,7 @@ libshibsp_la_SOURCES = \
 	remoting/impl/SecretSource.cpp \
 	remoting/impl/CurlHTTPRemotingService.cpp \
 	session/impl/AbstractSessionCache.cpp \
+	session/impl/CookieSessionCache.cpp \
 	session/impl/FilesystemSessionCache.cpp \
 	session/impl/MemorySessionCache.cpp \
 	session/impl/StorageServiceSessionCache.cpp \
diff --git a/shibsp/RequestMapper.h b/shibsp/RequestMapper.h
index ebd930bb..e7b7a538 100644
--- a/shibsp/RequestMapper.h
+++ b/shibsp/RequestMapper.h
@@ -73,6 +73,8 @@ namespace shibsp {
         static const char REQUIRE_SESSION_PROP_NAME[];
         static const char REQUIRE_LOGOUT_WITH_PROP_NAME[];
         static const char SESSION_COOKIE_NAME_PROP_NAME[];
+        static const char SESSION_COOKIE_CHUNKED_NAME_PROP_NAME[];
+        static const char SESSION_COOKIE_LAST_USED_NAME_PROP_NAME[];
         static const char SESSION_HOOK_PROP_NAME[];
         static const char TARGET_PROP_NAME[];
         static const char TIMEOUT_PROP_NAME[];
diff --git a/shibsp/impl/XMLRequestMapper.cpp b/shibsp/impl/XMLRequestMapper.cpp
index f8d70c3a..a7dd0da3 100644
--- a/shibsp/impl/XMLRequestMapper.cpp
+++ b/shibsp/impl/XMLRequestMapper.cpp
@@ -192,6 +192,8 @@ const char RequestMapper::REMOTE_USER_PROP_NAME[] =         "REMOTE_USER";
 const char RequestMapper::REQUIRE_LOGOUT_WITH_PROP_NAME[] = "requireLogoutWith";
 const char RequestMapper::REQUIRE_SESSION_PROP_NAME[] =     "requireSession";
 const char RequestMapper::SESSION_COOKIE_NAME_PROP_NAME[] = "sessionCookieName";
+const char RequestMapper::SESSION_COOKIE_CHUNKED_NAME_PROP_NAME[] = "sessionCookieChunkedName";
+const char RequestMapper::SESSION_COOKIE_LAST_USED_NAME_PROP_NAME[] = "sessionCookieLastUsedName";
 const char RequestMapper::SESSION_HOOK_PROP_NAME[] =        "sessionHook";
 const char RequestMapper::TARGET_PROP_NAME[] =              "target";
 const char RequestMapper::TIMEOUT_PROP_NAME[] =             "timeout";
diff --git a/shibsp/io/CookieManager.h b/shibsp/io/CookieManager.h
index 1b7ccaae..916ffeac 100644
--- a/shibsp/io/CookieManager.h
+++ b/shibsp/io/CookieManager.h
@@ -157,6 +157,15 @@ namespace shibsp {
          */
         void setSameSite(const char* sameSiteValue);
         
+        /**
+         * Sets the chunking size if using multiple cookies to store large values.
+         * 
+         * <p>Defaults to 3072.</p>
+         * 
+         * @param chunkSize max size of each cookie
+         */
+        void setChunkSize(unsigned int chunkSize);
+
         /**
          * Adds a cookie with the specified value to the outgoing response for the supplied request.
          * 
@@ -166,10 +175,14 @@ namespace shibsp {
          * <p>The value will NOT be encoded by this method and must be encoded by the caller
          * if necessary.</p>
          * 
+         * <p>Chunking is used for large values and will result in the data being split at
+         * the configured chunk size and stored in multiple related cookies.</p>
+         * 
          * @param request the request to operate on
          * @param value cookie value
+         * @param chunked whether to spread cookie value across multiple cookies
          */
-        void setCookie(SPRequest& request, const char* value) const;
+        void setCookie(SPRequest& request, const char* value, bool chunked=false) const;
 
         /**
          * Unsets a cookie via the outgoing response for the supplied request.
@@ -178,14 +191,15 @@ namespace shibsp {
          * associated with the request.</p>
          * 
          * @param request the request to operate on
+         * @param chunked whether to unset multiple cookies representing a chunked value
          */
-        void unsetCookie(SPRequest& request) const;
+        void unsetCookie(SPRequest& request, bool chunked=false) const;
 
         /**
          * Gets the value (if any) for the specified cookie.
          * 
-         * <p>If multiple cookies of the same name are present, it is unspecified
-         * which value will be returned.</p>
+         * <p>If multiple cookies of the same name are present or if chunking was used to create
+         * the value, it is unspecified which value will be returned.</p>
          * 
          * @param request the request to operate on
          * 
@@ -193,6 +207,28 @@ namespace shibsp {
          */
         const char* getCookieValue(const SPRequest& request) const;
         
+        /**
+         * Gets the value of a chunked cookie, combining the chunks into a final result.
+         * 
+         * @param request the request to operate on
+         * 
+         * @return a value for the named chunked cookie or an empty string
+         */
+        std::string getChunkedCookieValue(const SPRequest& request) const;
+
+        // Common setting names used in multiple places.
+        static const char COOKIE_NAME_PROP_NAME[];
+        static const char COOKIE_SECURE_PROP_NAME[];
+        static const char COOKIE_HTTPONLY_PROP_NAME[];
+        static const char COOKIE_PATH_PROP_NAME[];
+        static const char COOKIE_DOMAIN_PROP_NAME[];
+        static const char COOKIE_MAXAGE_PROP_NAME[];
+        static const char COOKIE_SAMESITE_PROP_NAME[];
+        static bool COOKIE_SECURE_PROP_DEFAULT;
+        static bool COOKIE_HTTPONLY_PROP_DEFAULT;
+        static const char COOKIE_PATH_PROP_DEFAULT[];
+        static int COOKIE_MAXAGE_PROP_DEFAULT;
+
     private:
         std::string computeCookieName(const SPRequest& request) const;
         void outputHeader(SPRequest& request, const char* value, int maxAge) const;
@@ -206,6 +242,7 @@ namespace shibsp {
         bool m_secure;
         bool m_httpOnly;
         samesite_t m_sameSiteValue;
+        unsigned int m_chunkSize;
     };
 
 #if defined (_MSC_VER)
diff --git a/shibsp/io/impl/CookieManager.cpp b/shibsp/io/impl/CookieManager.cpp
index 23b9e923..b9391ddd 100644
--- a/shibsp/io/impl/CookieManager.cpp
+++ b/shibsp/io/impl/CookieManager.cpp
@@ -38,6 +38,20 @@ using namespace std;
 # define strcasecmp _stricmp
 #endif
 
+const char CookieManager::COOKIE_NAME_PROP_NAME[] = "cookieName";
+const char CookieManager::COOKIE_SECURE_PROP_NAME[] = "cookieSecure";
+const char CookieManager::COOKIE_HTTPONLY_PROP_NAME[] = "cookieHttpOnly";
+const char CookieManager::COOKIE_PATH_PROP_NAME[] = "cookiePath";
+const char CookieManager::COOKIE_DOMAIN_PROP_NAME[] = "cookieDomain";
+const char CookieManager::COOKIE_MAXAGE_PROP_NAME[] = "cookieMaxAge";
+const char CookieManager::COOKIE_SAMESITE_PROP_NAME[] = "cookieSameSite";
+
+bool CookieManager::COOKIE_SECURE_PROP_DEFAULT = true;
+bool CookieManager::COOKIE_HTTPONLY_PROP_DEFAULT = true;
+const char CookieManager::COOKIE_PATH_PROP_DEFAULT[] = "/";
+int CookieManager::COOKIE_MAXAGE_PROP_DEFAULT = -1;
+
+
 CookieManager::CookieManager(const char* defaultName)
     : m_defaultName(defaultName),
         m_appSpecific(false),
@@ -45,7 +59,8 @@ CookieManager::CookieManager(const char* defaultName)
         m_maxAge(-1),
         m_secure(true),
         m_httpOnly(true),
-        m_sameSiteValue(SAMESITE_ABSENT)
+        m_sameSiteValue(SAMESITE_ABSENT),
+        m_chunkSize(3072)
 {
 }
 
@@ -110,6 +125,11 @@ void CookieManager::setSameSite(samesite_t value)
     m_sameSiteValue = value;
 }
 
+void CookieManager::setChunkSize(unsigned int chunkSize)
+{
+    m_chunkSize = chunkSize;
+}
+
 string CookieManager::computeCookieName(const SPRequest& request) const
 {
     // If not app-specific, return the overridden or default name unadorned.
@@ -186,12 +206,17 @@ const char* CookieManager::getCookieValue(const SPRequest& request) const
     return entry == cookies.end() ? nullptr : entry->second.c_str();
 }
 
-void CookieManager::setCookie(SPRequest& request, const char* value) const
+string CookieManager::getChunkedCookieValue(const SPRequest& request) const
+{
+    return string();
+}
+
+void CookieManager::setCookie(SPRequest& request, const char* value, bool chunked) const
 {
     outputHeader(request, value, request.getRequestSettings().first->getInt(RequestMapper::COOKIE_MAXAGE_PROP_NAME, m_maxAge));
 }
 
-void CookieManager::unsetCookie(SPRequest& request) const
+void CookieManager::unsetCookie(SPRequest& request, bool chunked) const
 {
     outputHeader(request, nullptr, 0);
 }
diff --git a/shibsp/session/SessionCache.h b/shibsp/session/SessionCache.h
index 0bb07a4c..a12e95ea 100644
--- a/shibsp/session/SessionCache.h
+++ b/shibsp/session/SessionCache.h
@@ -231,6 +231,9 @@ namespace shibsp {
         virtual void remove(const char* key)=0;
     };
 
+    /** SessionCache implementation backed by encrypted, chunked cookies. */
+    #define COOKIE_SESSION_CACHE    "cookie"
+
     /** SessionCache implementation backed by the file system. */
     #define FILESYSTEM_SESSION_CACHE    "filesystem"
 
diff --git a/shibsp/session/impl/AbstractSessionCache.cpp b/shibsp/session/impl/AbstractSessionCache.cpp
index 239c9cf4..50ca33b7 100644
--- a/shibsp/session/impl/AbstractSessionCache.cpp
+++ b/shibsp/session/impl/AbstractSessionCache.cpp
@@ -48,6 +48,7 @@ using namespace boost::property_tree;
 using namespace std;
 
 namespace shibsp {
+    extern SessionCache* SHIBSP_DLLLOCAL CookieSessionCacheFactory(ptree& pt, bool deprecationSupport);
     extern SessionCache* SHIBSP_DLLLOCAL FilesystemSessionCacheFactory(ptree& pt, bool deprecationSupport);
     extern SessionCache* SHIBSP_DLLLOCAL StorageServiceSessionCacheFactory(ptree& pt, bool deprecationSupport);
     extern SessionCache* SHIBSP_DLLLOCAL MemorySessionCacheFactory(ptree& pt, bool deprecationSupport);
@@ -58,23 +59,13 @@ static const char STORAGE_ACCESS_INTERVAL_PROP_NAME[] = "storageAccessInterval";
 static const char INPROC_TIMEOUT_PROP_NAME[] = "inprocTimeout";
 static const char ISSUER_ATTRIBUTE_PROP_NAME[] = "issuerAttribute";
 static const char UNRELIABLE_NETWORKS_PROP_NAME[] = "unreliableNetworks";
-static const char COOKIE_NAME_PROP_NAME[] = "cookieName";
-static const char COOKIE_SECURE_PROP_NAME[] = "cookieSecure";
-static const char COOKIE_HTTPONLY_PROP_NAME[] = "cookieHttpOnly";
-static const char COOKIE_PATH_PROP_NAME[] = "cookiePath";
-static const char COOKIE_DOMAIN_PROP_NAME[] = "cookieDomain";
-static const char COOKIE_MAXAGE_PROP_NAME[] = "cookieMaxAge";
-static const char COOKIE_SAMESITE_PROP_NAME[] = "cookieSameSite";
 
 static unsigned int CLEANUP_INTERVAL_PROP_DEFAULT = 900;
 static unsigned int STORAGE_ACCESS_INTERVAL_PROP_DEFAULT = 600;
 static unsigned int INPROC_TIMEOUT_PROP_DEFAULT = 900;
 static const char ISSUER_ATTRIBUTE_PROP_DEFAULT[] = "Shib-Identity-Provider";
+
 static const char COOKIE_NAME_PROP_DEFAULT[] = "__Host-shibsession";
-static bool COOKIE_SECURE_PROP_DEFAULT = true;
-static bool COOKIE_HTTPONLY_PROP_DEFAULT = true;
-static const char COOKIE_PATH_PROP_DEFAULT[] = "/";
-static int COOKIE_MAXAGE_PROP_DEFAULT = -1;
 
 // Logging Macros
 #define DEBUG_MARK request, m_log, Priority::SHIB_DEBUG
@@ -85,6 +76,7 @@ static int COOKIE_MAXAGE_PROP_DEFAULT = -1;
 
 void SHIBSP_API shibsp::registerSessionCaches()
 {
+    AgentConfig::getConfig().SessionCacheManager.registerFactory(COOKIE_SESSION_CACHE, CookieSessionCacheFactory);
     AgentConfig::getConfig().SessionCacheManager.registerFactory(FILESYSTEM_SESSION_CACHE, FilesystemSessionCacheFactory);
     AgentConfig::getConfig().SessionCacheManager.registerFactory(MEMORY_SESSION_CACHE, MemorySessionCacheFactory);
     AgentConfig::getConfig().SessionCacheManager.registerFactory(STORAGESERVICE_SESSION_CACHE, StorageServiceSessionCacheFactory);
@@ -211,14 +203,14 @@ AbstractSessionCache::AbstractSessionCache(const ptree& pt)
     m_storageAccessInterval = getUnsignedInt(STORAGE_ACCESS_INTERVAL_PROP_NAME, STORAGE_ACCESS_INTERVAL_PROP_DEFAULT);
 
     // Set up cookie manager.
-    m_cookieManager.reset(new CookieManager(getString(COOKIE_NAME_PROP_NAME, COOKIE_NAME_PROP_DEFAULT)));
+    m_cookieManager.reset(new CookieManager(getString(CookieManager::COOKIE_NAME_PROP_NAME, COOKIE_NAME_PROP_DEFAULT)));
     m_cookieManager->setCookieNamePolicy(RequestMapper::SESSION_COOKIE_NAME_PROP_NAME, true);
-    m_cookieManager->setSecure(getBool(COOKIE_SECURE_PROP_NAME, COOKIE_SECURE_PROP_DEFAULT));
-    m_cookieManager->setHttpOnly(getBool(COOKIE_HTTPONLY_PROP_NAME, COOKIE_HTTPONLY_PROP_DEFAULT));
-    m_cookieManager->setPath(getString(COOKIE_PATH_PROP_NAME, COOKIE_PATH_PROP_DEFAULT));
-    m_cookieManager->setMaxAge(getInt(COOKIE_MAXAGE_PROP_NAME, COOKIE_MAXAGE_PROP_DEFAULT));
-    m_cookieManager->setDomain(getString(COOKIE_DOMAIN_PROP_NAME));
-    m_cookieManager->setSameSite(getString(COOKIE_SAMESITE_PROP_NAME));
+    m_cookieManager->setSecure(getBool(CookieManager::COOKIE_SECURE_PROP_NAME, CookieManager::COOKIE_SECURE_PROP_DEFAULT));
+    m_cookieManager->setHttpOnly(getBool(CookieManager::COOKIE_HTTPONLY_PROP_NAME, CookieManager::COOKIE_HTTPONLY_PROP_DEFAULT));
+    m_cookieManager->setPath(getString(CookieManager::COOKIE_PATH_PROP_NAME, CookieManager::COOKIE_PATH_PROP_DEFAULT));
+    m_cookieManager->setMaxAge(getInt(CookieManager::COOKIE_MAXAGE_PROP_NAME, CookieManager::COOKIE_MAXAGE_PROP_DEFAULT));
+    m_cookieManager->setDomain(getString(CookieManager::COOKIE_DOMAIN_PROP_NAME));
+    m_cookieManager->setSameSite(getString(CookieManager::COOKIE_SAMESITE_PROP_NAME));
 
     const char* unreliableNetworks = getString(UNRELIABLE_NETWORKS_PROP_NAME);
     if (unreliableNetworks) {
@@ -692,6 +684,8 @@ bool AbstractSessionCache::update(SPRequest& request, unique_lock<Session>& sess
             return true;
         }
         else {
+            request.info("version mismatch on attempt to update session (%s), version (%u)",
+                session.mutex()->getID(), session.mutex()->getVersion());
             return false;
         }
     }
diff --git a/shibsp/session/impl/CookieSessionCache.cpp b/shibsp/session/impl/CookieSessionCache.cpp
new file mode 100644
index 00000000..8c8237c4
--- /dev/null
+++ b/shibsp/session/impl/CookieSessionCache.cpp
@@ -0,0 +1,526 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * session/impl/CookieSessionCache.cpp
+ *
+ * SessionCache implementation using encrypted cookies.
+ * 
+ * <p>This implementation relies on the Hub to encrypt and decrypt data
+ * that is chunked across multiple cookies of a configurable size.</p>
+ */
+
+#include "internal.h"
+#include "exceptions.h"
+#include "Agent.h"
+#include "AgentConfig.h"
+#include "SPRequest.h"
+#include "io/CookieManager.h"
+#include "remoting/RemotingService.h"
+#include "session/AbstractSessionCache.h"
+#include "logging/Category.h"
+#include "util/Date.h"
+#include "util/Misc.h"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/property_tree/ptree.hpp>
+
+using namespace shibsp;
+using namespace boost::property_tree;
+using namespace std;
+
+namespace {
+    class CookieSessionCache : public virtual AbstractSessionCache {
+    public:
+        CookieSessionCache(const ptree& pt);
+        ~CookieSessionCache();
+
+        string cache_create(SPRequest* request, const char* candidateSessionID, DDF& sessionData);
+        DDF cache_read(
+            SPRequest* request,
+            const char* applicationId,
+            const char* key,
+            unsigned int version=1,
+            unsigned int lifetime=0,
+            unsigned int timeout=0,
+            const char* client_addr=nullptr
+            );
+        bool cache_update(SPRequest* request, const char* key, unsigned int version, DDF& data);
+        bool cache_touch(SPRequest* request, const char* key, unsigned int version=1, unsigned int timeout=0);
+        void cache_remove(SPRequest* request, const char* key);
+    
+    private:
+        Category& m_spilog;
+        unsigned int m_cookieLifetime;
+        // We store the encrypted timestamp in a separate, unchunked cookie.
+        unique_ptr<CookieManager> m_lastUsedCookieManager;
+        unique_ptr<CookieManager> m_chunkedCookieManager;
+    };
+
+    static const char COOKIE_LIFETIME_PROP_NAME[] = "cookieLifetime";
+    static const char CHUNKED_COOKIE_NAME_PROP_NAME[] = "chunkedCookieName";
+    static const char LAST_USED_COOKIE_NAME_PROP_NAME[] = "lastUsedCookieName";
+
+    static unsigned int COOKIE_LIFETIME_PROP_DEFAULT = 24 * 3600;
+    static const char LAST_USED_COOKIE_NAME_PROP_DEFAULT[] = "__Host-shib_chk_ts";
+    static const char CHUNKED_COOKIE_NAME_PROP_DEFAULT[] = "__Host-shib_chk";
+};
+
+// Logging Macros
+#define DEBUG_MARK request, m_spilog, Priority::SHIB_DEBUG
+#define INFO_MARK request, m_spilog, Priority::SHIB_INFO
+#define WARN_MARK request, m_spilog, Priority::SHIB_WARN
+#define ERROR_MARK request, m_spilog, Priority::SHIB_ERROR
+#define CRIT_MARK request, m_spilog, Priority::SHIB_CRIT
+
+namespace shibsp {
+    SessionCache* SHIBSP_DLLLOCAL CookieSessionCacheFactory(ptree& pt, bool deprecationSupport) {
+        return new CookieSessionCache(pt);
+    }
+}
+
+CookieSessionCache::CookieSessionCache(const ptree& pt)
+    : AbstractSessionCache(pt), m_spilog(Category::getInstance(SHIBSP_LOGCAT ".SessionCache.StorageService"))
+{
+    m_cookieLifetime = getUnsignedInt(COOKIE_LIFETIME_PROP_NAME, COOKIE_LIFETIME_PROP_DEFAULT);
+
+    // Set up cookie managers.
+
+    m_lastUsedCookieManager.reset(new CookieManager(getString(LAST_USED_COOKIE_NAME_PROP_NAME, LAST_USED_COOKIE_NAME_PROP_DEFAULT)));
+    m_lastUsedCookieManager->setCookieNamePolicy(RequestMapper::SESSION_COOKIE_LAST_USED_NAME_PROP_NAME, true);
+    m_lastUsedCookieManager->setSecure(getBool(CookieManager::COOKIE_SECURE_PROP_NAME, CookieManager::COOKIE_SECURE_PROP_DEFAULT));
+    m_lastUsedCookieManager->setHttpOnly(getBool(CookieManager::COOKIE_HTTPONLY_PROP_NAME, CookieManager::COOKIE_HTTPONLY_PROP_DEFAULT));
+    m_lastUsedCookieManager->setPath(getString(CookieManager::COOKIE_PATH_PROP_NAME, CookieManager::COOKIE_PATH_PROP_DEFAULT));
+    m_lastUsedCookieManager->setMaxAge(getInt(CookieManager::COOKIE_MAXAGE_PROP_NAME, CookieManager::COOKIE_MAXAGE_PROP_DEFAULT));
+    m_lastUsedCookieManager->setDomain(getString(CookieManager::COOKIE_DOMAIN_PROP_NAME));
+    m_lastUsedCookieManager->setSameSite(getString(CookieManager::COOKIE_SAMESITE_PROP_NAME));
+
+    m_chunkedCookieManager.reset(new CookieManager(getString(CHUNKED_COOKIE_NAME_PROP_NAME, CHUNKED_COOKIE_NAME_PROP_DEFAULT)));
+    m_chunkedCookieManager->setCookieNamePolicy(RequestMapper::SESSION_COOKIE_CHUNKED_NAME_PROP_NAME, true);
+    m_chunkedCookieManager->setSecure(getBool(CookieManager::COOKIE_SECURE_PROP_NAME, CookieManager::COOKIE_SECURE_PROP_DEFAULT));
+    m_chunkedCookieManager->setHttpOnly(getBool(CookieManager::COOKIE_HTTPONLY_PROP_NAME, CookieManager::COOKIE_HTTPONLY_PROP_DEFAULT));
+    m_chunkedCookieManager->setPath(getString(CookieManager::COOKIE_PATH_PROP_NAME, CookieManager::COOKIE_PATH_PROP_DEFAULT));
+    m_chunkedCookieManager->setMaxAge(getInt(CookieManager::COOKIE_MAXAGE_PROP_NAME, CookieManager::COOKIE_MAXAGE_PROP_DEFAULT));
+    m_chunkedCookieManager->setDomain(getString(CookieManager::COOKIE_DOMAIN_PROP_NAME));
+    m_chunkedCookieManager->setSameSite(getString(CookieManager::COOKIE_SAMESITE_PROP_NAME));
+}
+
+CookieSessionCache::~CookieSessionCache()
+{
+}
+
+string CookieSessionCache::cache_create(SPRequest* request, const char* candidateSessionID, DDF& sessionData)
+{
+    if (!request) {
+        throw SessionException("Access to request object required to store cookie-backed session.");
+    }
+
+    // Put session ID in name of structure for later cross-check.
+    if (candidateSessionID) {
+        sessionData.name(candidateSessionID);
+    }
+    else {
+        sessionData.name(AgentConfig::getConfig().generateRandom(16));
+    }
+
+    // Serialize the data for encryption operation.
+    ostringstream sink;
+    sink << sessionData;
+
+    // Call for encryption of both the session and a timestamp.
+    const RemotingService* remoting = AgentConfig::getConfig().getAgent().getRemotingService();
+    DDF in = remoting->build("sealer", nullptr, request->getRequestID());
+    DDFJanitor injanitor(in);
+    DDF encList = in.addmember("encrypt").list();
+    DDF encChild1(nullptr);
+    encList.add(encChild1.string(sink.str()));
+
+    try {
+        // Inside try/catch in case it throws.
+        DDF encChild2(nullptr);
+        encList.add(encChild2.string(boost::lexical_cast<string>(time(nullptr))));
+
+        DDF out = remoting->send(in);
+        DDFJanitor outJanitor(out);
+        DDF encryptedList = out["encrypted"];
+        if (!encryptedList.islist() || encryptedList.integer() != 2) {
+            throw OperationException("No encrypted data returned from sealer operation.");
+        }
+
+        // Set the encrypted data via the chunking manager and the timestamp directly.
+        m_chunkedCookieManager->setCookie(*request, encryptedList.get(0).string(), true);
+        m_lastUsedCookieManager->setCookie(*request, encryptedList.get(1).string());
+    }
+    catch (const exception& e) {
+        log(ERROR_MARK, "exception attempting to encrypt session data via Hub: %s", e.what());
+        throw;
+    }
+
+    return sessionData.name();
+}
+
+DDF CookieSessionCache::cache_read(
+    SPRequest* request,
+    const char* applicationId,
+    const char* key,
+    unsigned int version,
+    unsigned int lifetime,
+    unsigned int timeout,
+    const char* client_addr
+    )
+{
+    if (!request) {
+        throw SessionException("Access to request object required to read cookie-backed session.");
+    }
+
+    string encryptedSession = m_chunkedCookieManager->getChunkedCookieValue(*request);
+    if (encryptedSession.empty()) {
+        log(DEBUG_MARK, "no cookie-backed session retreived from request");
+        return DDF();
+    }
+
+    const RemotingService* remoting = AgentConfig::getConfig().getAgent().getRemotingService();
+    DDF in = remoting->build("sealer", nullptr, request->getRequestID());
+    DDFJanitor injanitor(in);
+    DDF decList = in.addmember("decrypt").list();
+
+    if (timeout) {
+        const char* encryptedTimestamp = m_lastUsedCookieManager->getCookieValue(*request);
+        if (!encryptedTimestamp || !*encryptedTimestamp) {
+            log(WARN_MARK, "unable to load encrypted timestamp for session (%s) from cookie", key);
+            throw SessionException("Unable to load encrypted timestamp for session from cookie.");
+        }
+        DDF decChild1(nullptr);
+        decList.add((decChild1.string(encryptedTimestamp)));
+    }
+
+    DDF decChild2(nullptr);
+    decList.add((decChild2.string(encryptedSession)));
+
+    DDF out;
+    try {
+        // Add new timestamp to encrypt inside try/catch in case it throws a lexical_cast exception.
+        DDF encChild(nullptr);
+        in.addmember("encrypt").list().add(encChild.string(boost::lexical_cast<string>(time(nullptr))));
+        out = remoting->send(in);
+    }
+    catch (const exception& e) {
+        log(ERROR_MARK, "exception attempting to decrypt session (%s): %s", key, e.what());
+        throw;
+    }
+
+    DDFJanitor outjanitor(out);
+    DDF decryptedList = out["decrypted"];
+    if (!decryptedList.islist() || decryptedList.integer() != (timeout ? 2 : 1)) {
+        throw OperationException("No decrypted data returned from sealer operation.");
+    }
+    DDF encryptedList = out["encrypted"];
+    if (!encryptedList.islist() || encryptedList.integer() != 1) {
+        throw OperationException("No encrypted timestamp returned from sealer operation.");
+    }
+
+    DDF sessionData;
+    istringstream source(decryptedList.get(0).string());
+    source >> sessionData;
+    if (sessionData.isnull()) {
+        return DDF();
+    }
+
+    DDFJanitor sessionDataJanitor(sessionData);
+
+    // Check session ID.
+    if (!sessionData.name()) {
+        log(ERROR_MARK, "session did not carry an ID to cross check");
+        return DDF();
+    }
+    else if (strcmp(key, sessionData.name())) {
+        log(WARN_MARK, "session ID mismatch, requested (%s), decrypted (%s)", key, sessionData.name());
+        return DDF();
+    }
+
+    if (!isSessionDataValid(sessionData)) {
+        log(ERROR_MARK, "session data decrypted via Hub for (%s) was invalid", key);
+        throw IOException("Session data was invalid.");
+    }
+
+    // Check application. We know the member exists due to the previous check.
+    const char* appId = sessionData["app_id"].string();
+    if (strcmp(applicationId, appId)) {
+        log(WARN_MARK, "session (%s) issued for application (%s), accessed via application (%s)", key, appId, applicationId);
+        return DDF();
+    }
+
+    time_t now = time(nullptr);
+
+    if (timeout) {
+        const char* decryptedTimestamp = decryptedList.get(1).string();
+        time_t last_used = boost::lexical_cast<time_t>(decryptedTimestamp ? decryptedTimestamp : "0");
+        if (last_used + timeout < now) {
+            if (request->isPriorityEnabled(Priority::SHIB_INFO)) {
+                string ts(date::format("%FT%TZ", chrono::system_clock::from_time_t(last_used)));
+                log(INFO_MARK, "session (%s) expired for inactivity, timeout (%lu), last access (%s)", key, timeout, ts.c_str());
+            }
+            cache_remove(request, key);
+            return DDF();
+        }
+    }
+
+    if (lifetime) {
+        time_t notonorafter = sessionData["notonorafter"].longinteger();
+        if (notonorafter > 0 && notonorafter <= now) {
+            if (request->isPriorityEnabled(Priority::SHIB_INFO)) {
+                string expired(date::format("%FT%TZ", chrono::system_clock::from_time_t(notonorafter)));
+                log(INFO_MARK, "session (%s) has expired per NotOnOrAfter policy (%s)", key, expired.c_str());
+            }
+            cache_remove(request, key);
+            return DDF();
+        }
+
+        time_t start = sessionData["ts"].longinteger();
+        if (start + lifetime < now) {
+            if (request->isPriorityEnabled(Priority::SHIB_INFO)) {
+                string created(date::format("%FT%TZ", chrono::system_clock::from_time_t(start)));
+                string expired(date::format("%FT%TZ", chrono::system_clock::from_time_t(start + lifetime)));
+                log(INFO_MARK, "session (%s) has expired, created (%s), expired (%s)", key, created.c_str(), expired.c_str());
+            }
+            cache_remove(request, key);
+            return DDF();
+        }
+    }
+
+    if (client_addr) {
+        const char* family = getAddressFamily(client_addr);
+        const char* addr = sessionData[family].string();
+        if (addr) {
+            if (!isAddressMatch(client_addr, addr)) {
+                log(INFO_MARK, "session (%s) use invalid, bound to address (%s), accessed from (%s)", key, addr, client_addr);
+                return DDF();
+            }
+        }
+        else {
+            // We have to rebind the session to a new address family, requiring an update to the session.
+            log(INFO_MARK, "attempting update of session (%s) to rebind to new address (%s)", key, client_addr);
+
+            // Fill in the new address and attempt the update. We can do this using the "existing" output
+            // because we have the object janitor'd and the cache_update copies the object internally.
+            sessionData.addmember(family).string(client_addr);
+            try {
+                if (!cache_update(request, key, version, sessionData)) {
+                    // This signals a version mismatch in which the original session we were asked to read
+                    // has already been updated by another thread or process. In this case, we recurse the
+                    // read attempt. This has to terminate eventually...right?
+
+                    // We bump the version once, under the assunption it shouldn't be likely that it's been
+                    // updated behind us more than once...
+                    return cache_read(request, applicationId, key, version + 1, lifetime, timeout, client_addr);
+                }
+            }
+            catch (const exception& ex) {
+                // This is an outright error attempting the update, so we just fail hard.
+                log(ERROR_MARK, "exception attempting to update session (%s): %s", key, ex.what());
+                throw;
+            }
+        }
+    }    
+
+    // Otherwise update the timestamp cookie and return the session structure.
+    m_lastUsedCookieManager->setCookie(*request, encryptedList.get(0).string());
+    return sessionDataJanitor.release();
+}
+
+bool CookieSessionCache::cache_update(SPRequest* request, const char* key, unsigned int version, DDF& sessionData)
+{
+    if (!request) {
+        throw SessionException("Access to request object required to update cookie-backed session.");
+    }
+
+    // Note this is absolutely racy, as we don't have a syncronizable "back-end" to prevent races.
+    // The "last" one out will "win" and subsequent operations will detect mismatches.
+    
+    const RemotingService* remoting = AgentConfig::getConfig().getAgent().getRemotingService();
+    DDF in = remoting->build("sealer", nullptr, request->getRequestID());
+    DDFJanitor injanitor(in);
+
+    // We "assume" success to avoid an extra round trip, so we'll decrypt the existing data
+    // and encrypt the updated data and timestamp at the same time.
+
+    string encryptedSession = m_chunkedCookieManager->getChunkedCookieValue(*request);
+    if (encryptedSession.empty()) {
+        log(ERROR_MARK, "no cookie-backed session retreived from request");
+        throw SessionException("No cookie-backed session retrieved from request.");
+    }
+
+    DDF decChild(nullptr);
+    in.addmember("decrypt").list().add(decChild.string(encryptedSession));
+
+    // Ensure the new version is set accurately unless an error occurs.
+    sessionData.addmember("ver").integer(version + 1);
+
+    ostringstream sink;
+    sink << sessionData;
+
+    DDF encList = in.addmember("encrypt").list();
+    DDF encChild1(nullptr);
+    encList.add(encChild1.string(sink.str()));
+
+    DDF out;
+    try {
+        // Add new timestamp to encrypt inside try/catch in case it throws a lexical_cast exception.
+        DDF encChild2(nullptr);
+        encList.add(encChild2.string(boost::lexical_cast<string>(time(nullptr))));
+
+        out = remoting->send(in);
+    }
+    catch (const exception& e) {
+        log(ERROR_MARK, "exception attempting to encrypt/decrypt session (%s) via Hub: %s", key, e.what());
+        // Restore old version.
+        sessionData.addmember("ver").integer(version);
+        throw;
+    }
+
+    DDFJanitor outjanitor(out);
+
+    DDF decryptedList = out["decrypted"];
+    if (!decryptedList.islist() || decryptedList.integer() != 1) {
+        // Restore old version.
+        sessionData.addmember("ver").integer(version);
+        throw OperationException("No decrypted data returned from sealer operation.");
+    }
+    DDF encryptedList = out["encrypted"];
+    if (!encryptedList.islist() || encryptedList.integer() != 2) {
+        // Restore old version.
+        sessionData.addmember("ver").integer(version);
+        throw OperationException("No encrypted data returned from sealer operation.");
+    }
+
+    long oldVersion;
+    try {
+        // Reconstitute the old session to check for version mismatch.
+        istringstream source(decryptedList.get(0).string());
+        DDF oldSessionData;
+        source >> oldSessionData;
+        DDFJanitor oldSessionDataJanitor(oldSessionData);
+
+        oldVersion = oldSessionData["ver"].integer();
+        if (oldVersion == 0) {
+            // Implied version.
+            oldVersion = 1;
+        }
+    }
+    catch (const exception& e) {
+        // Restore old version.
+        sessionData.addmember("ver").integer(version);
+        throw;
+    }
+
+    // The decrypted session's version has to match the input version.
+    // Older should be impossible and treated as an error.
+    // Newer means a version mismatch, the session was updated by somebody else.
+
+    if (oldVersion > version) {
+        log(INFO_MARK, "decrypted session (%s) contains newer version (%ld) than input (%u)",
+            key, oldVersion, version);
+        // Restore old version.
+        sessionData.addmember("ver").integer(version);
+        return false;
+    }
+    else if (oldVersion < version) {
+        log(INFO_MARK, "decrypted session (%s) contains older version (%ld) than input (%u)",
+            key, oldVersion, version);
+        // Restore old version.
+        sessionData.addmember("ver").integer(version);
+        throw IOException("Unexpected out-of-date version found in decrypted session.");
+    }
+
+    m_chunkedCookieManager->setCookie(*request, encryptedList.get(0).string(), true);
+    m_lastUsedCookieManager->setCookie(*request, encryptedList.get(1).string());
+
+    return true;
+}
+
+bool CookieSessionCache::cache_touch(SPRequest* request, const char* key, unsigned int version, unsigned int timeout)
+{
+    if (!request) {
+        throw SessionException("Access to request object required to touch cookie-backed session.");
+    }
+
+    const RemotingService* remoting = AgentConfig::getConfig().getAgent().getRemotingService();
+    DDF in = remoting->build("sealer", nullptr, request->getRequestID());
+    DDFJanitor injanitor(in);
+
+    if (timeout) {
+        const char* encryptedTimestamp = m_lastUsedCookieManager->getCookieValue(*request);
+        if (!encryptedTimestamp || !*encryptedTimestamp) {
+            log(WARN_MARK, "unable to load encrypted timestamp for session (%s) from cookie", key);
+            return false;
+        }
+        DDF decChild(nullptr);
+        in.addmember("decrypt").list().add((decChild.string(encryptedTimestamp)));
+    }
+
+    DDF out;
+    try {
+        // Add new timestamp to encrypt inside try/catch in case it throws a lexical_cast exception.
+        DDF encChild(nullptr);
+        in.addmember("encrypt").list().add(encChild.string(boost::lexical_cast<string>(time(nullptr))));
+        out = remoting->send(in);
+    }
+    catch (const exception& e) {
+        log(ERROR_MARK, "exception attempting to encrypt/decrypt timestamps for session (%s): %s", key, e.what());
+        throw;
+    }
+
+    DDFJanitor outjanitor(out);
+    DDF decryptedList;
+    if (timeout) {
+        decryptedList = out["decrypted"];
+        if (!decryptedList.islist() || decryptedList.integer() != 1) {
+            throw OperationException("No decrypted timestamp returned from sealer operation.");
+        }
+    }
+    DDF encryptedList = out["encrypted"];
+    if (!encryptedList.islist() || encryptedList.integer() != 1) {
+        throw OperationException("No encrypted timestamp returned from sealer operation.");
+    }
+
+
+    if (timeout) {
+        const char* decryptedTimestamp = decryptedList.get(0).string();
+        time_t last_used = boost::lexical_cast<time_t>(decryptedTimestamp ? decryptedTimestamp : "0");
+        if (last_used + timeout < time(nullptr)) {
+            if (request->isPriorityEnabled(Priority::SHIB_INFO)) {
+                string ts(date::format("%FT%TZ", chrono::system_clock::from_time_t(last_used)));
+                log(INFO_MARK, "session (%s) expired for inactivity, timeout (%lu), last access (%s)", key, timeout, ts.c_str());
+            }
+            cache_remove(request, key);
+            return false;
+        }
+    }
+
+    // Update timestamp cookie.
+    m_lastUsedCookieManager->setCookie(*request, encryptedList.get(0).string());
+    return true;
+}
+
+void CookieSessionCache::cache_remove(SPRequest* request, const char* key)
+{
+    if (!request) {
+        throw SessionException("Access to request object required to remove cookie-backed session.");
+    }
+
+    m_chunkedCookieManager->unsetCookie(*request, true);
+    m_lastUsedCookieManager->unsetCookie(*request);
+
+    log(INFO_MARK, "removed cookie-backed session (%s)", key);
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list