From c794ea976db7dcd8d6142ac8fc68966725cba8d7 Mon Sep 17 00:00:00 2001
From: Bruce Toll <btoll@dhsus.com>
Date: Sun, 7 Jun 2020 16:31:24 -0400
Subject: [PATCH] WIP WResource sequences and setInvalidAfterChanged

Use independent "rand" query parameters for each private resource to
prevent leaking information about how many accesses a private resource
has had (from other sessions).

Also add support for setInvalidAfterChanged option on WResource
which causes a 404 Page Not Found for outdated rand values.

The setInvalidAfterChanged could arguably be made a protected method.

The 404 mechanism may need some refinement.

Should probably include some test code.
---
 src/Wt/WApplication.C | 41 +++++++++++++++++++++++++++++++++++------
 src/Wt/WApplication.h | 13 +++++++++++--
 src/Wt/WResource.C    |  6 ++++++
 src/Wt/WResource.h    | 21 +++++++++++++++++++++
 src/web/WebSession.C  | 16 +++++++++++++---
 5 files changed, 86 insertions(+), 11 deletions(-)

diff --git a/src/Wt/WApplication.C b/src/Wt/WApplication.C
index a3458288..62367c9b 100644
--- a/src/Wt/WApplication.C
+++ b/src/Wt/WApplication.C
@@ -72,6 +72,11 @@ WApplication::MetaLink::MetaLink(const std::string &aHref,
     sizes(aSizes), disabled(aDisabled)
 { }
 
+WApplication::ExposedResourceInfo::ExposedResourceInfo(WResource* resource,
+                                                       unsigned long sequence)
+  : resource(resource), sequence(sequence)
+{ }
+
 bool WApplication::ScriptLibrary::operator< (const ScriptLibrary& other) const
 {
   return uri < other.uri;
@@ -863,18 +868,25 @@ std::string WApplication::resourceMapKey(WResource *resource)
 
 std::string WApplication::addExposedResource(WResource *resource)
 {
-  exposedResources_[resourceMapKey(resource)] = resource;
+  ExposedResourceInfo exposedResourceInfo(resource);
+  std::string key = resourceMapKey(resource);
+
+  ResourceMap::const_iterator i = exposedResources_.find(key);
+  if (i != exposedResources_.end())
+    exposedResourceInfo = i->second;
+
+  exposedResourceInfo.sequence++;
+
+  exposedResources_[key] = exposedResourceInfo;
 
   std::string fn = resource->suggestedFileName().toUTF8();
   if (!fn.empty() && fn[0] != '/')
     fn = '/' + fn;
 
-  static unsigned long seq = 0;
-
   if (resource->internalPath().empty())
     return session_->mostRelativeUrl(fn)
       + "&request=resource&resource=" + Utils::urlEncode(resource->id())
-      + "&rand=" + std::to_string(seq++);
+      + "&rand=" + std::to_string(exposedResourceInfo.sequence);
   else {
     fn = resource->internalPath() + fn;
     if (!session_->applicationName().empty() && fn[0] != '/')
@@ -888,7 +900,7 @@ bool WApplication::removeExposedResource(WResource *resource)
   std::string key = resourceMapKey(resource);
   ResourceMap::iterator i = exposedResources_.find(key);
 
-  if (i != exposedResources_.end() && i->second == resource) {
+  if (i != exposedResources_.end() && i->second.resource == resource) {
 #ifndef WT_TARGET_JAVA
     exposedResources_.erase(i);
 #else
@@ -905,7 +917,7 @@ WResource *WApplication::decodeExposedResource(const std::string& resourceKey)
   ResourceMap::const_iterator i = exposedResources_.find(resourceKey);
   
   if (i != exposedResources_.end())
-    return i->second;
+    return i->second.resource;
   else {
     std::size_t j = resourceKey.rfind('/');
     if (j != std::string::npos && j > 1)
@@ -915,6 +927,23 @@ WResource *WApplication::decodeExposedResource(const std::string& resourceKey)
   }
 }
 
+WResource *WApplication::decodeExposedResource(const std::string& resourceKey,
+                                               unsigned long rand) const
+{
+  ResourceMap::const_iterator i = exposedResources_.find(resourceKey);
+
+  WResource *resource = nullptr;
+  if (i != exposedResources_.end())
+    resource = i->second.resource;
+
+  if (resource
+      && resource->invalidAfterChanged()
+      && (i->second.sequence != rand))
+    resource = nullptr;
+
+  return resource;
+}
+
 std::string WApplication::encodeObject(WObject *object)
 {
   std::string result = "w" + object->uniqueId();
diff --git a/src/Wt/WApplication.h b/src/Wt/WApplication.h
index 924de13c..639e8823 100644
--- a/src/Wt/WApplication.h
+++ b/src/Wt/WApplication.h
@@ -2230,12 +2230,19 @@ private:
     bool disabled;
   };
 
+  struct ExposedResourceInfo {
+    ExposedResourceInfo(WResource* resource=nullptr, unsigned long sequence=0);
+
+    WResource* resource;
+    unsigned long sequence;
+  };
+
 #ifndef WT_TARGET_JAVA
   typedef std::map<std::string, EventSignalBase *> SignalMap;
-  typedef std::map<std::string, WResource *> ResourceMap;
+  typedef std::map<std::string, ExposedResourceInfo> ResourceMap;
 #else
   typedef std::weak_value_map<std::string, EventSignalBase *> SignalMap;
-  typedef std::weak_value_map<std::string, WResource *> ResourceMap;
+  typedef std::weak_value_map<std::string, ExposedResourceInfo> ResourceMap;
 #endif
   typedef std::map<std::string, WObject *> ObjectMap;
 
@@ -2336,6 +2343,8 @@ private:
   std::string addExposedResource(WResource *resource);
   bool removeExposedResource(WResource *resource);
   WResource *decodeExposedResource(const std::string& resourceMapKey) const;
+  WResource *decodeExposedResource(const std::string& resourceMapKey,
+                                   unsigned long rand) const;
 
   /*
    * Methods for application state handling
diff --git a/src/Wt/WResource.C b/src/Wt/WResource.C
index 53e534f8..084ec78c 100644
--- a/src/Wt/WResource.C
+++ b/src/Wt/WResource.C
@@ -68,6 +68,7 @@ WResource::UseLock::~UseLock()
 WResource::WResource()
   : trackUploadProgress_(false),
     takesUpdateLock_(false),
+    invalidAfterChanged_(false),
     dispositionType_(ContentDisposition::None),
     app_(nullptr)
 { 
@@ -284,6 +285,11 @@ void WResource::setChanged()
   dataChanged_.emit();
 }
 
+void WResource::setInvalidAfterChanged(bool enabled)
+{
+  invalidAfterChanged_ = enabled;
+}
+
 const std::string& WResource::url() const
 {
   if (currentUrl_.empty())
diff --git a/src/Wt/WResource.h b/src/Wt/WResource.h
index 70a2dc69..9730344f 100644
--- a/src/Wt/WResource.h
+++ b/src/Wt/WResource.h
@@ -239,9 +239,29 @@ public:
    *
    * This does not work when the resource is deployed at an internal path using
    * setInternalPath().
+   *
+   * \sa setInvalidAfterChanged()
    */
   void setChanged();
 
+  /*! \brief Return "page not found" for prior resource URLs after change
+   *
+   * This option invalidates earlier versions of the resource url prior to
+   * the last call of setChanged().
+   *
+   * This does not work when the resource is deployed at an internal path using
+   * setInternalPath().
+   *
+   * \sa setChanged()
+   */
+  void setInvalidAfterChanged(bool enabled);
+
+  /*! \brief Should "page not found" be returned for outdated resource URLs
+   *
+   * \sa setInvalidAfterChanged()
+   */
+  bool invalidAfterChanged() const { return invalidAfterChanged_; }
+
   /*! \brief Sets an internal path for this resource.
    *
    * Using this method you can deploy the resource at a fixed path. Unless
@@ -448,6 +468,7 @@ private:
 
   bool trackUploadProgress_;
   bool takesUpdateLock_;
+  bool invalidAfterChanged_;
 
   std::vector<Http::ResponseContinuationPtr> continuations_;
 
diff --git a/src/web/WebSession.C b/src/web/WebSession.C
index 84c58474..2dc1e660 100644
--- a/src/web/WebSession.C
+++ b/src/web/WebSession.C
@@ -2416,6 +2416,7 @@ void WebSession::notify(const WEvent& event)
 
       const std::string *resourceE = request.getParameter("resource");
       const std::string *signalE = getSignal(request, "");
+      const std::string *randE = request.getParameter("rand");
 
       if (signalE)
 	progressiveBoot_ = false; 
@@ -2428,8 +2429,16 @@ void WebSession::notify(const WEvent& event)
 	    "<body> </body></html>";
 	  handler.flushResponse();
 	} else {
-	  if (!resource)
-	    resource = app_->decodeExposedResource(*resourceE);
+	  if (!resource) {
+            long rand = 0;
+            try {
+              if (randE)
+                rand = Utils::stol(*randE);
+            } catch (std::invalid_argument) {
+              rand = 0;
+            }
+	    resource = app_->decodeExposedResource(*resourceE, rand);
+          }
 
 	  if (resource) {
 	    try {
@@ -2447,7 +2456,8 @@ void WebSession::notify(const WEvent& event)
 		      << "' not exposed");
 	    handler.response()->setContentType("text/html");
 	    handler.response()->out() <<
-	      "<html><body><h1>Nothing to say about that.</h1></body></html>";
+	      "<html><body><h1>Page not found.</h1></body></html>";
+            handler.response()->setStatus(404);
 	    handler.flushResponse();
 	  }
 	}
-- 
2.29.2

