diff OrthancFramework/Sources/HttpServer/HttpServer.cpp @ 4228:c8c0bbaaace3

write access to webdav
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 06 Oct 2020 12:45:11 +0200
parents 7fff7e683d65
children 013d6c6b2387
line wrap: on
line diff
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Mon Oct 05 10:55:24 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Tue Oct 06 12:45:11 2020 +0200
@@ -45,7 +45,9 @@
 #  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
 #    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
 #  endif
-
+#  if !defined(CIVETWEB_HAS_WEBDAV_WRITING)
+#    error Macro CIVETWEB_HAS_WEBDAV_WRITING must be defined
+#  endif
 #else
 #  error "Either Mongoose or Civetweb must be enabled to compile this file"
 #endif
@@ -343,6 +345,38 @@
   }
                                                   
 
+  static PostDataStatus ReadBodyWithoutContentLength(std::string& body,
+                                                     struct mg_connection *connection)
+  {
+    // Store the individual chunks in a temporary file, then read it
+    // back into the memory buffer "body"
+    FileBuffer buffer;
+
+    std::string tmp(1024 * 1024, 0);
+      
+    for (;;)
+    {
+      int r = mg_read(connection, &tmp[0], tmp.size());
+      if (r < 0)
+      {
+        return PostDataStatus_Failure;
+      }
+      else if (r == 0)
+      {
+        break;
+      }
+      else
+      {
+        buffer.Append(tmp.c_str(), r);
+      }
+    }
+
+    buffer.Read(body);
+
+    return PostDataStatus_Success;
+  }
+                                                  
+
   static PostDataStatus ReadBodyToString(std::string& body,
                                          struct mg_connection *connection,
                                          const IHttpHandler::Arguments& headers)
@@ -356,32 +390,8 @@
     }
     else
     {
-      // No Content-Length. Store the individual chunks in a temporary
-      // file, then read it back into the memory buffer "body"
-      FileBuffer buffer;
-
-      std::string tmp(1024 * 1024, 0);
-      
-      for (;;)
-      {
-        int r = mg_read(connection, &tmp[0], tmp.size());
-        if (r < 0)
-        {
-          return PostDataStatus_Failure;
-        }
-        else if (r == 0)
-        {
-          break;
-        }
-        else
-        {
-          buffer.Append(tmp.c_str(), r);
-        }
-      }
-
-      buffer.Read(body);
-
-      return PostDataStatus_Success;
+      // No Content-Length
+      return ReadBodyWithoutContentLength(body, connection);
     }
   }
 
@@ -696,11 +706,35 @@
   }
 
 
-  bool HttpServer::HandleWebDav(HttpOutput& output,
-                                const std::string& method,
-                                const IHttpHandler::Arguments& headers,
-                                const std::string& uri)
+#if ORTHANC_ENABLE_PUGIXML == 1
+
+#  if CIVETWEB_HAS_WEBDAV_WRITING == 0
+  static void AnswerWebDavReadOnly(HttpOutput& output,
+                                   const std::string& uri)
   {
+    LOG(ERROR) << "Orthanc was compiled without support for read-write access to WebDAV: " << uri;
+    output.SendStatus(HttpStatus_403_Forbidden);
+  }    
+#  endif
+  
+  static bool HandleWebDav(HttpOutput& output,
+                           const HttpServer::WebDavBuckets& buckets,
+                           const std::string& method,
+                           const IHttpHandler::Arguments& headers,
+                           const std::string& uri,
+                           struct mg_connection *connection /* to read the PUT body if need be */)
+  {
+    if (buckets.empty())
+    {
+      return false;  // Speed up things if WebDAV is not used
+    }
+    
+    /**
+     * The "buckets" maps an URI relative to the root of the
+     * bucket, to the content of the bucket. The root URI does *not*
+     * contain a trailing slash.
+     **/
+    
     if (method == "OPTIONS")
     {
       // Remove the trailing slash, if any (necessary for davfs2)
@@ -711,38 +745,37 @@
         s.resize(s.size() - 1);
       }
       
-      WebDavBuckets::const_iterator bucket = webDavBuckets_.find(s);
-      if (bucket == webDavBuckets_.end())
+      HttpServer::WebDavBuckets::const_iterator bucket = buckets.find(s);
+      if (bucket == buckets.end())
       {
         return false;
       }
       else
       {
-        output.AddHeader("DAV", "1");
-        output.AddHeader("Allow", "GET, POST, PUT, DELETE, PROPFIND");
+        output.AddHeader("DAV", "1,2");  // Necessary for Windows XP
+
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1
+        output.AddHeader("Allow", "GET, PUT, OPTIONS, PROPFIND, HEAD, LOCK, UNLOCK, PROPPATCH, MKCOL");
+#else
+        output.AddHeader("Allow", "GET, PUT, OPTIONS, PROPFIND, HEAD");
+#endif
+
         output.SendStatus(HttpStatus_200_Ok);
         return true;
       }
     }
-    else if (method == "PROPFIND")
+    else if (method == "GET" ||
+             method == "PROPFIND" ||
+             method == "PROPPATCH" ||
+             method == "PUT" ||
+             method == "HEAD" ||
+             method == "LOCK" ||
+             method == "UNLOCK" ||
+             method == "MKCOL")
     {
-      IHttpHandler::Arguments::const_iterator i = headers.find("depth");
-      if (i == headers.end())
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "WebDAV PROPFIND without depth");
-      }
-
-      int depth = boost::lexical_cast<int>(i->second);
-      if (depth != 0 &&
-          depth != 1)
-      {
-        throw OrthancException(
-          ErrorCode_NetworkProtocol,
-          "WebDAV PROPFIND at unsupported depth (can only be 0 or 1): " + i->second);
-      }
-      
-      for (WebDavBuckets::const_iterator bucket = webDavBuckets_.begin();
-           bucket != webDavBuckets_.end(); ++bucket)
+      // Locate the WebDAV bucket of interest, if any
+      for (HttpServer::WebDavBuckets::const_iterator bucket = buckets.begin();
+           bucket != buckets.end(); ++bucket)
       {
         assert(!bucket->first.empty() &&
                bucket->first[bucket->first.size() - 1] != '/' &&
@@ -760,42 +793,222 @@
           std::vector<std::string> path;
           Toolbox::SplitUriComponents(path, s);
 
-          std::string answer;
+
+          /**
+           * WebDAV - PROPFIND
+           **/
+          
+          if (method == "PROPFIND")
+          {
+            IHttpHandler::Arguments::const_iterator i = headers.find("depth");
+            if (i == headers.end())
+            {
+              throw OrthancException(ErrorCode_NetworkProtocol, "WebDAV PROPFIND without depth");
+            }
+
+            int depth = boost::lexical_cast<int>(i->second);
+            if (depth != 0 &&
+                depth != 1)
+            {
+              throw OrthancException(
+                ErrorCode_NetworkProtocol,
+                "WebDAV PROPFIND at unsupported depth (can only be 0 or 1): " + i->second);
+            }
+      
+            std::string answer;
           
-          if (depth == 0)
-          {
-            if (bucket->second->IsExistingFolder(path))
+            if (depth == 0)
+            {
+              MimeType mime;
+              std::string content;
+              boost::posix_time::ptime modificationTime;
+            
+              if (bucket->second->IsExistingFolder(path))
+              {
+                IWebDavBucket::Collection c;
+                c.AddResource(new IWebDavBucket::Folder(""));
+                c.Format(answer, uri);
+              }
+              else if (!path.empty() &&
+                       bucket->second->GetFileContent(mime, content, modificationTime, path))
+              {
+                std::unique_ptr<IWebDavBucket::File> f(new IWebDavBucket::File(path.back()));
+                f->SetContentLength(content.size());
+                f->SetModificationTime(modificationTime);
+                f->SetMimeType(mime);
+
+                IWebDavBucket::Collection c;
+                c.AddResource(f.release());
+
+                std::vector<std::string> p;
+                Toolbox::SplitUriComponents(p, uri);
+                if (p.empty())
+                {
+                  throw OrthancException(ErrorCode_InternalError);
+                }
+                
+                p.resize(p.size() - 1);
+                c.Format(answer, Toolbox::FlattenUri(p));
+              }
+              else
+              {
+                output.SendStatus(HttpStatus_404_NotFound);
+                return true;
+              }
+            }
+            else if (depth == 1)
             {
               IWebDavBucket::Collection c;
-              c.AddResource(new IWebDavBucket::Folder(""));
+              c.AddResource(new IWebDavBucket::Folder(""));  // Necessary for empty folders
+              
+              if (!bucket->second->ListCollection(c, path))
+              {
+                output.SendStatus(HttpStatus_404_NotFound);
+                return true;
+              }
+
               c.Format(answer, uri);
             }
             else
             {
+              throw OrthancException(ErrorCode_InternalError);
+            }
+
+            output.AddHeader("Content-Type", "application/xml; charset=UTF-8");
+            output.SendStatus(HttpStatus_207_MultiStatus, answer);
+            return true;
+          }
+          
+
+          /**
+           * WebDAV - GET and HEAD
+           **/
+          
+          else if (method == "GET" ||
+                   method == "HEAD")
+          {
+            MimeType mime;
+            std::string content;
+            boost::posix_time::ptime modificationTime;
+            
+            if (bucket->second->GetFileContent(mime, content, modificationTime, path))
+            {
+              output.AddHeader("Content-Type", EnumerationToString(mime));
+
+              // "Last-Modified" is necessary on Windows XP. The "Z"
+              // suffix is mandatory on Windows >= 7.
+              output.AddHeader("Last-Modified", boost::posix_time::to_iso_extended_string(modificationTime) + "Z");
+
+              if (method == "GET")
+              {
+                output.Answer(content);
+              }
+              else
+              {
+                output.SendStatus(HttpStatus_200_Ok);
+              }
+            }
+            else
+            {
               output.SendStatus(HttpStatus_404_NotFound);
-              return true;
-            }
-          }
-          else if (depth == 1)
-          {
-            IWebDavBucket::Collection c;
-            if (!bucket->second->ListCollection(c, path))
-            {
-              output.SendStatus(HttpStatus_404_NotFound);
-              return true;
             }
 
-            c.Format(answer, uri);
+            return true;
+          }
+
+          
+          /**
+           * WebDAV - PUT
+           **/
+          
+          else if (method == "PUT")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            std::string body;
+            if (ReadBodyToString(body, connection, headers) == PostDataStatus_Success)
+            {
+              if (bucket->second->StoreFile(body, path))
+              {
+                //output.SendStatus(HttpStatus_200_Ok);
+                output.SendStatus(HttpStatus_201_Created);
+              }
+              else
+              {
+                output.SendStatus(HttpStatus_403_Forbidden);
+              }
+            }
+            else
+            {
+              LOG(ERROR) << "Cannot read the content of a file to be stored in WebDAV";
+              output.SendStatus(HttpStatus_400_BadRequest);
+            }
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+
+            return true;
+          }
+          
+
+          /**
+           * WebDAV - MKCOL
+           **/
+          
+          else if (method == "MKCOL")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            if (bucket->second->CreateFolder(path))
+            {
+              //output.SendStatus(HttpStatus_200_Ok);
+              output.SendStatus(HttpStatus_201_Created);
+            }
+            else
+            {
+              output.SendStatus(HttpStatus_403_Forbidden);
+            }
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+
+            return true;
+          }
+          
+
+          /**
+           * WebDAV - Faking PROPPATCH, LOCK and UNLOCK
+           **/
+          
+          else if (method == "PROPPATCH")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            IWebDavBucket::AnswerFakedProppatch(output, uri);
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+            return true;
+          }
+          else if (method == "LOCK")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            IWebDavBucket::AnswerFakedLock(output, uri);
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+            return true;
+          }
+          else if (method == "UNLOCK")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            IWebDavBucket::AnswerFakedUnlock(output);
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+            return true;
           }
           else
           {
             throw OrthancException(ErrorCode_InternalError);
           }
-          
-          output.AddHeader("DAV", "1");
-          output.AddHeader("Content-Type", "application/xml");
-          output.SendStatus(HttpStatus_207_MultiStatus, answer);
-          return true;
         }
       }
       
@@ -803,11 +1016,16 @@
     }
     else
     {
+      /**
+       * WebDAV - Unapplicable method (such as POST and DELETE)
+       **/
+          
       return false;
     }
-  }
+  } 
+#endif /* ORTHANC_ENABLE_PUGIXML == 1 */
 
-
+  
   static void InternalCallback(HttpOutput& output /* out */,
                                HttpMethod& method /* out */,
                                HttpServer& server,
@@ -910,21 +1128,39 @@
     // Compute the HTTP method, taking method faking into consideration
     method = HttpMethod_Get;
 
+#if ORTHANC_ENABLE_PUGIXML == 1
     bool isWebDav = false;
+#endif
+    
     HttpMethod filterMethod;
+
     
     if (ExtractMethod(method, request, headers, argumentsGET))
     {
       LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri);
       filterMethod = method;
     }
+#if ORTHANC_ENABLE_PUGIXML == 1
     else if (!strcmp(request->request_method, "OPTIONS") ||
-             !strcmp(request->request_method, "PROPFIND"))
+             !strcmp(request->request_method, "PROPFIND") ||
+             !strcmp(request->request_method, "HEAD"))
     {
-      LOG(INFO) << "Incoming WebDAV request: " << request->request_method << " " << requestUri;
+      LOG(INFO) << "Incoming read-only WebDAV request: "
+                << request->request_method << " " << requestUri;
       filterMethod = HttpMethod_Get;
       isWebDav = true;
     }
+    else if (!strcmp(request->request_method, "PROPPATCH") ||
+             !strcmp(request->request_method, "LOCK") ||
+             !strcmp(request->request_method, "UNLOCK") ||
+             !strcmp(request->request_method, "MKCOL"))
+    {
+      LOG(INFO) << "Incoming read-write WebDAV request: "
+                << request->request_method << " " << requestUri;
+      filterMethod = HttpMethod_Put;
+      isWebDav = true;
+    }
+#endif /* ORTHANC_ENABLE_PUGIXML == 1 */
     else
     {
       LOG(INFO) << "Unknown HTTP method: " << request->request_method;
@@ -949,7 +1185,9 @@
     }
 
 
-    if (server.HandleWebDav(output, request->request_method, headers, requestUri))
+#if ORTHANC_ENABLE_PUGIXML == 1
+    if (HandleWebDav(output, server.GetWebDavBuckets(), request->request_method,
+                     headers, requestUri, connection))
     {
       return;
     }
@@ -960,6 +1198,7 @@
       output.SendStatus(HttpStatus_404_NotFound);
       return;
     }
+#endif
 
 
     bool found = false;
@@ -1381,6 +1620,14 @@
         }
       }
 
+#if ORTHANC_ENABLE_PUGIXML == 1    
+      for (WebDavBuckets::iterator it = webDavBuckets_.begin(); it != webDavBuckets_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        it->second->Start();
+      }
+#endif
+
       LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber()
                    << " (HTTPS encryption is "
                    << (IsSslEnabled() ? "enabled" : "disabled")
@@ -1395,6 +1642,15 @@
     if (IsRunning())
     {
       mg_stop(pimpl_->context_);
+      
+#if ORTHANC_ENABLE_PUGIXML == 1    
+      for (WebDavBuckets::iterator it = webDavBuckets_.begin(); it != webDavBuckets_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        it->second->Stop();
+      }
+#endif
+
       pimpl_->context_ = NULL;
     }
   }