# HG changeset patch # User Sebastien Jodogne # Date 1601981111 -7200 # Node ID c8c0bbaaace30da70da54802f60459ec93e564b9 # Parent 7fff7e683d65c9ad42f71ef356bb7a3b3c4b7067 write access to webdav diff -r 7fff7e683d65 -r c8c0bbaaace3 OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake --- a/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake Mon Oct 05 10:55:24 2020 +0200 +++ b/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake Tue Oct 06 12:45:11 2020 +0200 @@ -88,6 +88,7 @@ add_definitions( -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1 + -DCIVETWEB_HAS_WEBDAV_WRITING=1 ) else() @@ -112,10 +113,16 @@ # up multipart transfers, as encountered in DICOMweb. CHECK_LIBRARY_EXISTS(civetweb mg_disable_keep_alive "" CIVETWEB_HAS_DISABLE_KEEP_ALIVE) if (CIVETWEB_HAS_DISABLE_KEEP_ALIVE) - add_definitions(-DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1) + add_definitions( + -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1 + -DCIVETWEB_HAS_WEBDAV_WRITING=1 + ) message("Performance: Your system-wide distribution of civetweb is configured for best performance") else() - message(WARNING "Performance: Your system-wide distribution of civetweb does not feature the mg_disable_keep_alive() function") - add_definitions(-DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=0) + message(WARNING "Performance: Your system-wide distribution of civetweb does not feature the mg_disable_keep_alive() function, and WebDAV will only be available for read-only access") + add_definitions( + -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=0 + -DCIVETWEB_HAS_WEBDAV_WRITING=0 + ) endif() endif() diff -r 7fff7e683d65 -r c8c0bbaaace3 OrthancFramework/Resources/Patches/civetweb-1.12.patch --- a/OrthancFramework/Resources/Patches/civetweb-1.12.patch Mon Oct 05 10:55:24 2020 +0200 +++ b/OrthancFramework/Resources/Patches/civetweb-1.12.patch Tue Oct 06 12:45:11 2020 +0200 @@ -1,6 +1,6 @@ diff -urEb civetweb-1.12.orig/include/civetweb.h civetweb-1.12/include/civetweb.h ---- civetweb-1.12.orig/include/civetweb.h 2020-04-02 12:07:20.727054140 +0200 -+++ civetweb-1.12/include/civetweb.h 2020-04-02 12:07:42.734996559 +0200 +--- civetweb-1.12.orig/include/civetweb.h 2020-10-06 12:39:10.634902843 +0200 ++++ civetweb-1.12/include/civetweb.h 2020-10-06 12:39:30.630872089 +0200 @@ -1614,6 +1614,9 @@ struct mg_error_data *error); #endif @@ -12,9 +12,21 @@ } #endif /* __cplusplus */ diff -urEb civetweb-1.12.orig/src/civetweb.c civetweb-1.12/src/civetweb.c ---- civetweb-1.12.orig/src/civetweb.c 2020-04-02 12:07:20.731054129 +0200 -+++ civetweb-1.12/src/civetweb.c 2020-04-02 12:07:52.250971600 +0200 -@@ -20704,5 +20704,12 @@ +--- civetweb-1.12.orig/src/civetweb.c 2020-10-06 12:39:10.638902837 +0200 ++++ civetweb-1.12/src/civetweb.c 2020-10-06 12:41:40.110671929 +0200 +@@ -10525,6 +10525,11 @@ + /* + MicroSoft extensions + * https://msdn.microsoft.com/en-us/library/aa142917.aspx */ + ++ /* Added by SJ, for write access to WebDAV on Windows >= 7 */ ++ {"LOCK", 1, 1, 0, 0, 0}, ++ {"UNLOCK", 1, 0, 0, 0, 0}, ++ {"PROPPATCH", 1, 1, 0, 0, 0}, ++ + /* REPORT method (RFC 3253) */ + {"REPORT", 1, 1, 1, 1, 1}, + /* REPORT method only allowed for CGI/Lua/LSP and callbacks. */ +@@ -20704,5 +20709,12 @@ return 1; } diff -r 7fff7e683d65 -r c8c0bbaaace3 OrthancFramework/Sources/HttpServer/HttpServer.cpp --- 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(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 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(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 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 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; } } diff -r 7fff7e683d65 -r c8c0bbaaace3 OrthancFramework/Sources/HttpServer/HttpServer.h --- a/OrthancFramework/Sources/HttpServer/HttpServer.h Mon Oct 05 10:55:24 2020 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpServer.h Tue Oct 06 12:45:11 2020 +0200 @@ -76,6 +76,11 @@ class ORTHANC_PUBLIC HttpServer : public boost::noncopyable { + public: +#if ORTHANC_ENABLE_PUGIXML == 1 + typedef std::map WebDavBuckets; +#endif + private: // http://stackoverflow.com/questions/311166/stdauto-ptr-or-boostshared-ptr-for-pimpl-idiom struct PImpl; @@ -103,7 +108,6 @@ unsigned int requestTimeout_; // In seconds #if ORTHANC_ENABLE_PUGIXML == 1 - typedef std::map WebDavBuckets; WebDavBuckets webDavBuckets_; #endif @@ -236,13 +240,15 @@ } #if ORTHANC_ENABLE_PUGIXML == 1 + WebDavBuckets& GetWebDavBuckets() + { + return webDavBuckets_; + } +#endif + +#if ORTHANC_ENABLE_PUGIXML == 1 void Register(const std::vector& root, IWebDavBucket* bucket); // Takes ownership #endif - - bool HandleWebDav(HttpOutput& output, - const std::string& method, - const IHttpHandler::Arguments& headers, - const std::string& uri); }; } diff -r 7fff7e683d65 -r c8c0bbaaace3 OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp --- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp Mon Oct 05 10:55:24 2020 +0200 +++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp Tue Oct 06 12:45:11 2020 +0200 @@ -23,6 +23,7 @@ #include "../PrecompiledHeaders.h" #include "IWebDavBucket.h" +#include "HttpOutput.h" #include "../OrthancException.h" #include "../Toolbox.h" @@ -90,7 +91,7 @@ void IWebDavBucket::Resource::Format(pugi::xml_node& node, - const std::string& parentPath) const + const std::string& parentPath) const { node.set_name("D:response"); @@ -98,8 +99,11 @@ node.append_child("D:href").append_child(pugi::node_pcdata).set_value(s.c_str()); pugi::xml_node propstat = node.append_child("D:propstat"); - propstat.append_child("D:status").append_child(pugi::node_pcdata). - set_value("HTTP/1.1 200 OK"); + + static const HttpStatus status = HttpStatus_200_Ok; + s = ("HTTP/1.1 " + boost::lexical_cast(status) + " " + + std::string(EnumerationToString(status))); + propstat.append_child("D:status").append_child(pugi::node_pcdata).set_value(s.c_str()); pugi::xml_node prop = propstat.append_child("D:prop"); @@ -111,6 +115,12 @@ prop.append_child("D:getlastmodified").append_child(pugi::node_pcdata).set_value(s.c_str()); #if 0 + // Maybe used by davfs2 + prop.append_child("D:quota-available-bytes"); + prop.append_child("D:quota-used-bytes"); +#endif + +#if 0 prop.append_child("D:lockdiscovery"); pugi::xml_node lock = prop.append_child("D:supportedlock"); @@ -217,4 +227,84 @@ Toolbox::XmlToString(target, doc); } + + + void IWebDavBucket::AnswerFakedProppatch(HttpOutput& output, + const std::string& uri) + { + /** + * This is a fake implementation. The goal is to make happy the + * WebDAV clients that set properties (such as Windows >= 7). + **/ + + pugi::xml_document doc; + + pugi::xml_node root = doc.append_child("D:multistatus"); + root.append_attribute("xmlns:D").set_value("DAV:"); + + pugi::xml_node response = root.append_child("D:response"); + response.append_child("D:href").append_child(pugi::node_pcdata).set_value(uri.c_str()); + + response.append_child("D:propstat"); + + pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); + decl.append_attribute("version").set_value("1.0"); + decl.append_attribute("encoding").set_value("UTF-8"); + + std::string s; + Toolbox::XmlToString(s, doc); + + output.AddHeader("Content-Type", "application/xml"); + output.SendStatus(HttpStatus_207_MultiStatus, s); + } + + + void IWebDavBucket::AnswerFakedLock(HttpOutput& output, + const std::string& uri) + { + /** + * This is a fake implementation. No lock is actually + * created. The goal is to make happy the WebDAV clients + * that use locking (such as Windows >= 7). + **/ + + pugi::xml_document doc; + + pugi::xml_node root = doc.append_child("D:prop"); + root.append_attribute("xmlns:D").set_value("DAV:"); + + pugi::xml_node activelock = root.append_child("D:lockdiscovery").append_child("D:activelock"); + activelock.append_child("D:locktype").append_child("D:write"); + activelock.append_child("D:lockscope").append_child("D:exclusive"); + activelock.append_child("D:depth").append_child(pugi::node_pcdata).set_value("0"); + activelock.append_child("D:timeout").append_child(pugi::node_pcdata).set_value("Second-3599"); + + activelock.append_child("D:lockroot").append_child("D:href") + .append_child(pugi::node_pcdata).set_value(uri.c_str()); + activelock.append_child("D:owner"); + + std::string token = Toolbox::GenerateUuid(); + boost::erase_all(token, "-"); + token = "opaquelocktoken:0x" + token; + + activelock.append_child("D:locktoken").append_child("D:href"). + append_child(pugi::node_pcdata).set_value(token.c_str()); + + pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); + decl.append_attribute("version").set_value("1.0"); + decl.append_attribute("encoding").set_value("UTF-8"); + + std::string s; + Toolbox::XmlToString(s, doc); + + output.AddHeader("Lock-Token", token); // Necessary for davfs2 + output.AddHeader("Content-Type", "application/xml"); + output.SendStatus(HttpStatus_201_Created, s); + } + + + void IWebDavBucket::AnswerFakedUnlock(HttpOutput& output) + { + output.SendStatus(HttpStatus_204_NoContent); + } } diff -r 7fff7e683d65 -r c8c0bbaaace3 OrthancFramework/Sources/HttpServer/IWebDavBucket.h --- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.h Mon Oct 05 10:55:24 2020 +0200 +++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h Tue Oct 06 12:45:11 2020 +0200 @@ -41,6 +41,8 @@ namespace Orthanc { + class HttpOutput; + class IWebDavBucket : public boost::noncopyable { public: @@ -115,6 +117,8 @@ return mime_; } + void SetCreated(bool created); + virtual void Format(pugi::xml_node& node, const std::string& parentPath) const ORTHANC_OVERRIDE; }; @@ -146,7 +150,7 @@ void Format(std::string& target, const std::string& parentPath) const; }; - + virtual ~IWebDavBucket() { @@ -157,7 +161,30 @@ virtual bool ListCollection(Collection& collection, const std::vector& path) = 0; - virtual bool GetFileContent(std::string& content, + virtual bool GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& modificationTime, const std::vector& path) = 0; + + // "false" returns indicate a read-only target + virtual bool StoreFile(const std::string& content, + const std::vector& path) = 0; + + virtual bool CreateFolder(const std::vector& path) = 0; + + virtual void Start() = 0; + + // During the shutdown of the Web server, give a chance to the + // bucket to end its pending operations + virtual void Stop() = 0; + + + static void AnswerFakedProppatch(HttpOutput& output, + const std::string& uri); + + static void AnswerFakedLock(HttpOutput& output, + const std::string& uri); + + static void AnswerFakedUnlock(HttpOutput& output); }; } diff -r 7fff7e683d65 -r c8c0bbaaace3 OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Mon Oct 05 10:55:24 2020 +0200 +++ b/OrthancServer/Sources/main.cpp Tue Oct 06 12:45:11 2020 +0200 @@ -613,29 +613,290 @@ +static const char* const UPLOAD_FOLDER = "upload"; + class DummyBucket : public IWebDavBucket // TODO { private: ServerContext& context_; + static void RemoveFirstElement(std::vector& target, + const std::vector& source) + { + if (source.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + target.resize(source.size() - 1); + for (size_t i = 1; i < source.size(); i++) + { + target[i - 1] = source[i]; + } + } + } + + class UploadedFile : public boost::noncopyable + { + private: + std::string content_; + MimeType mime_; + boost::posix_time::ptime time_; + + void Touch() + { + time_ = boost::posix_time::second_clock::universal_time(); + } + + public: + UploadedFile() : + mime_(MimeType_Binary) + { + Touch(); + } + + void SetContent(const std::string& content, + MimeType mime) + { + content_ = content; + mime_ = mime; + Touch(); + } + + MimeType GetMimeType() const + { + return mime_; + } + + const std::string& GetContent() const + { + return content_; + } + + const boost::posix_time::ptime& GetTime() const + { + return time_; + } + }; + + + class UploadedFolder : public boost::noncopyable + { + private: + typedef std::map Files; + typedef std::map Subfolders; + + Files files_; + Subfolders subfolders_; + + void CheckName(const std::string& name) + { + if (name.empty() || + name.find('/') != std::string::npos || + name.find('\\') != std::string::npos || + name.find('\0') != std::string::npos) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad resource name for WebDAV: " + name); + } + } + + bool IsExisting(const std::string& name) const + { + return (files_.find(name) != files_.end() || + subfolders_.find(name) != subfolders_.end()); + } + + public: + ~UploadedFolder() + { + for (Files::iterator it = files_.begin(); it != files_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + for (Subfolders::iterator it = subfolders_.begin(); it != subfolders_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + const UploadedFile* LookupFile(const std::string& name) const + { + Files::const_iterator found = files_.find(name); + if (found == files_.end()) + { + return NULL; + } + else + { + assert(found->second != NULL); + return found->second; + } + } + + bool CreateSubfolder(const std::string& name) + { + CheckName(name); + + if (IsExisting(name)) + { + LOG(ERROR) << "WebDAV folder already existing: " << name; + return false; + } + else + { + subfolders_[name] = new UploadedFolder; + return true; + } + } + + bool StoreFile(const std::string& name, + const std::string& content, + MimeType mime) + { + CheckName(name); + + if (subfolders_.find(name) != subfolders_.end()) + { + LOG(ERROR) << "WebDAV folder already existing: " << name; + return false; + } + + Files::iterator found = files_.find(name); + if (found == files_.end()) + { + std::unique_ptr f(new UploadedFile); + f->SetContent(content, mime); + files_[name] = f.release(); + } + else + { + assert(found->second != NULL); + found->second->SetContent(content, mime); + } + + return true; + } + + UploadedFolder* LookupSubfolder(const UriComponents& path) + { + if (path.empty()) + { + return this; + } + else + { + Subfolders::const_iterator found = subfolders_.find(path[0]); + if (found == subfolders_.end()) + { + return NULL; + } + else + { + assert(found->second != NULL); + + UriComponents p; + RemoveFirstElement(p, path); + + return found->second->LookupSubfolder(p); + } + } + } + + void ListCollection(Collection& collection) const + { + for (Files::const_iterator it = files_.begin(); it != files_.end(); ++it) + { + assert(it->second != NULL); + + std::unique_ptr f(new File(it->first)); + f->SetContentLength(it->second->GetContent().size()); + f->SetCreationTime(it->second->GetTime()); + collection.AddResource(f.release()); + } + + for (Subfolders::const_iterator it = subfolders_.begin(); it != subfolders_.end(); ++it) + { + collection.AddResource(new Folder(it->first)); + } + } + }; + + bool IsUploadedFolder(const UriComponents& path) const + { + return (path.size() >= 1 && + path[0] == UPLOAD_FOLDER); + } + + UploadedFolder uploads_; + boost::recursive_mutex mutex_; + + UploadedFolder* LookupUploadedFolder(const UriComponents& path) + { + if (IsUploadedFolder(path)) + { + UriComponents p; + RemoveFirstElement(p, path); + + return uploads_.LookupSubfolder(p); + } + else + { + return NULL; + } + } + public: DummyBucket(ServerContext& context) : context_(context) { } - virtual bool IsExistingFolder(const std::vector& path) ORTHANC_OVERRIDE + virtual bool IsExistingFolder(const UriComponents& path) ORTHANC_OVERRIDE { - return (path.size() == 0 || - (path.size() == 1 && path[0] == "Folder1") || - (path.size() == 2 && path[0] == "Folder1" && path[1] == "Folder2")); + boost::recursive_mutex::scoped_lock lock(mutex_); + + if (IsUploadedFolder(path)) + { + return LookupUploadedFolder(path) != NULL; + } + else + { + return (path.size() == 0 || + (path.size() == 1 && path[0] == "Folder1") || + (path.size() == 2 && path[0] == "Folder1" && path[1] == "Folder2")); + } } virtual bool ListCollection(Collection& collection, const UriComponents& path) ORTHANC_OVERRIDE { - if (IsExistingFolder(path)) + boost::recursive_mutex::scoped_lock lock(mutex_); + + if (IsUploadedFolder(path)) { + const UploadedFolder* folder = LookupUploadedFolder(path); + if (folder == NULL) + { + return false; + } + else + { + folder->ListCollection(collection); + return true; + } + } + else if (IsExistingFolder(path)) + { + if (path.empty()) + { + collection.AddResource(new Folder(UPLOAD_FOLDER)); + } + for (unsigned int i = 0; i < 5; i++) { std::unique_ptr f(new File("IM" + boost::lexical_cast(i) + ".dcm")); @@ -648,7 +909,65 @@ { collection.AddResource(new Folder("Folder" + boost::lexical_cast(i))); } - + + return true; + } + else + { + return false; + } + } + + virtual bool GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& modificationTime, + const UriComponents& path) ORTHANC_OVERRIDE + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + if (path.empty()) + { + return false; + } + else if (IsUploadedFolder(path)) + { + std::vector p(path.begin(), path.end() - 1); + + const UploadedFolder* folder = LookupUploadedFolder(p); + if (folder == NULL) + { + return false; + } + + const UploadedFile* file = folder->LookupFile(path.back()); + if (file == NULL) + { + return false; + } + else + { + mime = file->GetMimeType(); + content = file->GetContent(); + modificationTime = file->GetTime(); + return true; + } + } + else if (path.back() == "IM0.dcm" || + path.back() == "IM1.dcm" || + path.back() == "IM2.dcm" || + path.back() == "IM3.dcm" || + path.back() == "IM4.dcm") + { + modificationTime = boost::posix_time::second_clock::universal_time(); + + std::string s; + for (size_t i = 0; i < path.size(); i++) + { + s += "/" + path[i]; + } + + content = "Hello world!\r\n" + s + "\r\n"; + mime = MimeType_PlainText; return true; } else @@ -657,17 +976,75 @@ } } - virtual bool GetFileContent(std::string& content, - const UriComponents& path) ORTHANC_OVERRIDE + + virtual bool StoreFile(const std::string& content, + const UriComponents& path) ORTHANC_OVERRIDE { - std::string s = "/"; - for (size_t i = 0; i < path.size(); i++) + boost::recursive_mutex::scoped_lock lock(mutex_); + + if (IsUploadedFolder(path)) + { + std::vector p(path.begin(), path.end() - 1); + + UploadedFolder* folder = LookupUploadedFolder(p); + if (folder == NULL) + { + return false; + } + else + { + printf("STORING %d bytes at %s\n", content.size(), path.back().c_str()); + return folder->StoreFile(path.back(), content, SystemToolbox::AutodetectMimeType(path.back())); + } + } + else + { + LOG(WARNING) << "Writing to a read-only location in WebDAV: " << Toolbox::FlattenUri(path); + return false; + } + } + + + virtual bool CreateFolder(const UriComponents& path) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + if (IsUploadedFolder(path)) { - s += path[i] + "/"; - } + std::vector p(path.begin(), path.end() - 1); - content = "Hello world!\r\n" + s + "\r\n"; - return true; + UploadedFolder* folder = LookupUploadedFolder(p); + if (folder == NULL) + { + return false; + } + else + { + printf("CREATING FOLDER %s\n", path.back().c_str()); + return folder->CreateSubfolder(path.back()); + } + } + else + { + LOG(WARNING) << "Writing to a read-only location in WebDAV: " << Toolbox::FlattenUri(path); + return false; + } + } + + + virtual void Start() ORTHANC_OVERRIDE + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + LOG(WARNING) << "Starting WebDAV"; + } + + + virtual void Stop() ORTHANC_OVERRIDE + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + LOG(WARNING) << "Stopping WebDAV"; } }; @@ -1111,7 +1488,7 @@ httpServer.Register(context.GetHttpHandler()); { - std::vector root; // TODO + UriComponents root; // TODO root.push_back("a"); root.push_back("b"); httpServer.Register(root, new DummyBucket(context));