1 /**
2  * Orthanc - A Lightweight, RESTful DICOM Store
3  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4  * Department, University Hospital of Liege, Belgium
5  * Copyright (C) 2017-2021 Osimis S.A., Belgium
6  *
7  * This program is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public License
9  * as published by the Free Software Foundation, either version 3 of
10  * the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this program. If not, see
19  * <http://www.gnu.org/licenses/>.
20  **/
21 
22 
23 #include "../PrecompiledHeaders.h"
24 #include "IWebDavBucket.h"
25 
26 #include "HttpOutput.h"
27 #include "../OrthancException.h"
28 #include "../Toolbox.h"
29 
30 
GetNow()31 static boost::posix_time::ptime GetNow()
32 {
33   return boost::posix_time::second_clock::universal_time();
34 }
35 
36 
AddTrailingSlash(const std::string & s)37 static std::string AddTrailingSlash(const std::string& s)
38 {
39   if (s.empty() ||
40       s[s.size() - 1] != '/')
41   {
42     return s + '/';
43   }
44   else
45   {
46     return s;
47   }
48 }
49 
50 
51 namespace Orthanc
52 {
Resource(const std::string & displayName)53   IWebDavBucket::Resource::Resource(const std::string& displayName) :
54     displayName_(displayName),
55     hasModificationTime_(false),
56     creationTime_(GetNow()),
57     modificationTime_(GetNow())
58   {
59     if (displayName.empty() ||
60         displayName.find('/') != std::string::npos ||
61         displayName.find('\\') != std::string::npos ||
62         displayName.find('\0') != std::string::npos)
63     {
64       throw OrthancException(ErrorCode_ParameterOutOfRange,
65                              "Bad resource name for WebDAV: " + displayName);
66     }
67   }
68 
69 
SetCreationTime(const boost::posix_time::ptime & t)70   void IWebDavBucket::Resource::SetCreationTime(const boost::posix_time::ptime& t)
71   {
72     if (t.is_special())
73     {
74       throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time");
75     }
76     else
77     {
78       creationTime_ = t;
79 
80       if (!hasModificationTime_)
81       {
82         modificationTime_ = t;
83       }
84     }
85   }
86 
87 
SetModificationTime(const boost::posix_time::ptime & t)88   void IWebDavBucket::Resource::SetModificationTime(const boost::posix_time::ptime& t)
89   {
90     if (t.is_special())
91     {
92       throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time");
93     }
94     else
95     {
96       modificationTime_ = t;
97       hasModificationTime_ = true;
98     }
99   }
100 
101 
FormatInternal(pugi::xml_node & node,const std::string & href,const std::string & displayName,const boost::posix_time::ptime & creationTime,const boost::posix_time::ptime & modificationTime)102   static void FormatInternal(pugi::xml_node& node,
103                              const std::string& href,
104                              const std::string& displayName,
105                              const boost::posix_time::ptime& creationTime,
106                              const boost::posix_time::ptime& modificationTime)
107   {
108     node.set_name("D:response");
109 
110     node.append_child("D:href").append_child(pugi::node_pcdata).set_value(href.c_str());
111 
112     pugi::xml_node propstat = node.append_child("D:propstat");
113 
114     static const HttpStatus status = HttpStatus_200_Ok;
115     std::string s = ("HTTP/1.1 " + boost::lexical_cast<std::string>(status) + " " +
116                      std::string(EnumerationToString(status)));
117     propstat.append_child("D:status").append_child(pugi::node_pcdata).set_value(s.c_str());
118 
119     pugi::xml_node prop = propstat.append_child("D:prop");
120     prop.append_child("D:displayname").append_child(pugi::node_pcdata).set_value(displayName.c_str());
121 
122     // IMPORTANT: Adding the "Z" suffix is mandatory on Windows >= 7 (it indicates UTC)
123     assert(!creationTime.is_special());
124     s = boost::posix_time::to_iso_extended_string(creationTime) + "Z";
125     prop.append_child("D:creationdate").append_child(pugi::node_pcdata).set_value(s.c_str());
126 
127     assert(!modificationTime.is_special());
128     s = boost::posix_time::to_iso_extended_string(modificationTime) + "Z";
129     prop.append_child("D:getlastmodified").append_child(pugi::node_pcdata).set_value(s.c_str());
130 
131 #if 0
132     // Maybe used by davfs2
133     prop.append_child("D:quota-available-bytes");
134     prop.append_child("D:quota-used-bytes");
135 #endif
136 
137 #if 0
138     prop.append_child("D:lockdiscovery");
139     pugi::xml_node lock = prop.append_child("D:supportedlock");
140 
141     pugi::xml_node lockentry = lock.append_child("D:lockentry");
142     lockentry.append_child("D:lockscope").append_child("D:exclusive");
143     lockentry.append_child("D:locktype").append_child("D:write");
144 
145     lockentry = lock.append_child("D:lockentry");
146     lockentry.append_child("D:lockscope").append_child("D:shared");
147     lockentry.append_child("D:locktype").append_child("D:write");
148 #endif
149   }
150 
151 
File(const std::string & displayName)152   IWebDavBucket::File::File(const std::string& displayName) :
153     Resource(displayName),
154     contentLength_(0),
155     mime_(MimeType_Binary)
156   {
157   }
158 
159 
Format(pugi::xml_node & node,const std::string & parentPath) const160   void IWebDavBucket::File::Format(pugi::xml_node& node,
161                                    const std::string& parentPath) const
162   {
163     std::string href;
164     Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName());
165     FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime());
166 
167     pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
168     prop.append_child("D:resourcetype");
169 
170     std::string s = boost::lexical_cast<std::string>(contentLength_);
171     prop.append_child("D:getcontentlength").append_child(pugi::node_pcdata).set_value(s.c_str());
172 
173     s = EnumerationToString(mime_);
174     prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value(s.c_str());
175   }
176 
177 
Format(pugi::xml_node & node,const std::string & parentPath) const178   void IWebDavBucket::Folder::Format(pugi::xml_node& node,
179                                      const std::string& parentPath) const
180   {
181     std::string href;
182     Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName());
183     FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime());
184 
185     pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
186     prop.append_child("D:resourcetype").append_child("D:collection");
187 
188     //prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value("httpd/unix-directory");
189   }
190 
191 
~Collection()192   IWebDavBucket::Collection::~Collection()
193   {
194     for (std::list<Resource*>::iterator it = resources_.begin(); it != resources_.end(); ++it)
195     {
196       assert(*it != NULL);
197       delete(*it);
198     }
199   }
200 
201 
AddResource(Resource * resource)202   void IWebDavBucket::Collection::AddResource(Resource* resource)  // Takes ownership
203   {
204     if (resource == NULL)
205     {
206       throw OrthancException(ErrorCode_NullPointer);
207     }
208     else
209     {
210       resources_.push_back(resource);
211     }
212   }
213 
214 
ListDisplayNames(std::set<std::string> & target)215   void IWebDavBucket::Collection::ListDisplayNames(std::set<std::string>& target)
216   {
217     for (std::list<Resource*>::iterator it = resources_.begin(); it != resources_.end(); ++it)
218     {
219       assert(*it != NULL);
220       target.insert((*it)->GetDisplayName());
221     }
222   }
223 
224 
Format(std::string & target,const std::string & parentPath) const225   void IWebDavBucket::Collection::Format(std::string& target,
226                                          const std::string& parentPath) const
227   {
228     pugi::xml_document doc;
229 
230     pugi::xml_node root = doc.append_child("D:multistatus");
231     root.append_attribute("xmlns:D").set_value("DAV:");
232 
233     {
234       pugi::xml_node self = root.append_child();
235 
236       std::vector<std::string> tokens;
237       Toolbox::SplitUriComponents(tokens, parentPath);
238 
239       std::string folder;
240       if (!tokens.empty())
241       {
242         folder = tokens.back();
243       }
244 
245       std::string href;
246       Toolbox::UriEncode(href, Toolbox::FlattenUri(tokens) + "/");
247 
248       boost::posix_time::ptime now = GetNow();
249       FormatInternal(self, href, folder, now, now);
250 
251       pugi::xml_node prop = self.first_element_by_path("D:propstat/D:prop");
252       prop.append_child("D:resourcetype").append_child("D:collection");
253     }
254 
255     for (std::list<Resource*>::const_iterator
256            it = resources_.begin(); it != resources_.end(); ++it)
257     {
258       assert(*it != NULL);
259       pugi::xml_node n = root.append_child();
260       (*it)->Format(n, parentPath);
261     }
262 
263     pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
264     decl.append_attribute("version").set_value("1.0");
265     decl.append_attribute("encoding").set_value("UTF-8");
266 
267     Toolbox::XmlToString(target, doc);
268   }
269 
270 
AnswerFakedProppatch(HttpOutput & output,const std::string & uri)271   void IWebDavBucket::AnswerFakedProppatch(HttpOutput& output,
272                                            const std::string& uri)
273   {
274     /**
275      * This is a fake implementation. The goal is to make happy the
276      * WebDAV clients that set properties (such as Windows >= 7).
277      **/
278 
279     pugi::xml_document doc;
280 
281     pugi::xml_node root = doc.append_child("D:multistatus");
282     root.append_attribute("xmlns:D").set_value("DAV:");
283 
284     pugi::xml_node response = root.append_child("D:response");
285     response.append_child("D:href").append_child(pugi::node_pcdata).set_value(uri.c_str());
286 
287     response.append_child("D:propstat");
288 
289     pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
290     decl.append_attribute("version").set_value("1.0");
291     decl.append_attribute("encoding").set_value("UTF-8");
292 
293     std::string s;
294     Toolbox::XmlToString(s, doc);
295 
296     output.AddHeader("Content-Type", "application/xml");
297     output.SendStatus(HttpStatus_207_MultiStatus, s);
298   }
299 
300 
AnswerFakedLock(HttpOutput & output,const std::string & uri)301   void IWebDavBucket::AnswerFakedLock(HttpOutput& output,
302                                       const std::string& uri)
303   {
304     /**
305      * This is a fake implementation. No lock is actually
306      * created. The goal is to make happy the WebDAV clients
307      * that use locking (such as Windows >= 7).
308      **/
309 
310     pugi::xml_document doc;
311 
312     pugi::xml_node root = doc.append_child("D:prop");
313     root.append_attribute("xmlns:D").set_value("DAV:");
314 
315     pugi::xml_node activelock = root.append_child("D:lockdiscovery").append_child("D:activelock");
316     activelock.append_child("D:locktype").append_child("D:write");
317     activelock.append_child("D:lockscope").append_child("D:exclusive");
318     activelock.append_child("D:depth").append_child(pugi::node_pcdata).set_value("0");
319     activelock.append_child("D:timeout").append_child(pugi::node_pcdata).set_value("Second-3599");
320 
321     activelock.append_child("D:lockroot").append_child("D:href")
322       .append_child(pugi::node_pcdata).set_value(uri.c_str());
323     activelock.append_child("D:owner");
324 
325     std::string token = Toolbox::GenerateUuid();
326     boost::erase_all(token, "-");
327     token = "opaquelocktoken:0x" + token;
328 
329     activelock.append_child("D:locktoken").append_child("D:href").
330       append_child(pugi::node_pcdata).set_value(token.c_str());
331 
332     pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
333     decl.append_attribute("version").set_value("1.0");
334     decl.append_attribute("encoding").set_value("UTF-8");
335 
336     std::string s;
337     Toolbox::XmlToString(s, doc);
338 
339     output.AddHeader("Lock-Token", token);  // Necessary for davfs2
340     output.AddHeader("Content-Type", "application/xml");
341     output.SendStatus(HttpStatus_201_Created, s);
342   }
343 
344 
AnswerFakedUnlock(HttpOutput & output)345   void IWebDavBucket::AnswerFakedUnlock(HttpOutput& output)
346   {
347     output.SendStatus(HttpStatus_204_NoContent);
348   }
349 }
350